Developer Guide
Developer Guide
Architecture Overview
MVC Pattern
Flatboard 5 follows the Model-View-Controller (MVC) pattern:
app/
âââ Controllers/ # Handle requests and logic
âââ Models/ # Data models and business logic
âââ Views/ # Template files
âââ Core/ # Core framework classes
âââ Helpers/ # Helper functions
âââ Middleware/ # Request middleware
âââ Services/ # Service classesDirectory Structure
Flatboard5/
âââ app/ # Application code
â âââ Controllers/ # Controllers
â âââ Models/ # Models
â âââ Views/ # Views
â âââ Core/ # Core classes
â âââ Helpers/ # Helpers
â âââ Middleware/ # Middleware
â âââ Services/ # Services
âââ public/ # Public files
â âââ index.php # Entry point
âââ stockage/ # Data storage
âââ uploads/ # User uploads
âââ plugins/ # Plugins
âââ themes/ # Themes
âââ vendor/ # DependenciesCore Components
Autoloader
PSR-4 autoloading:
use App\Core\Autoloader;
Autoloader::register(BASE_PATH);Router
Route registration (the Router requires Request and Response instances):
use App\Core\Router;
use App\Core\Request;
use App\Core\Response;
$router = new Router($request, $response);
$router->get('/path', 'Controller@method');
$router->post('/path', 'Controller@method');
// Named routes
$router->get('/forum', 'ForumController@index')->name('forum.index');
$url = $router->url('forum.index'); // Generate URL
$url = $router->url('discussion.show', ['id' => 42]); // With params
// Route groups (shared prefix + middleware)
$router->group(['prefix' => '/admin', 'middleware' => ['App\Middleware\AuthMiddleware']], function($router) {
$router->get('/dashboard', 'Admin\DashboardController@index');
});
// Parameter constraints
$router->get('/user/{id}', 'UserController@show')->where('id', '[0-9]+');
// RESTful resource routes (generates GET list, GET show, POST, PUT, DELETE)
$router->resource('/posts', 'PostController');
// Regex-based routes
$router->regex('GET', '#^/custom/(.+)$#', function($matches) { /* ... */ });
// Global before/after hooks (for monitoring, logging)
$router->beforeEach(function($request) { /* ... */ });
$router->afterEach(function($request, $response) { /* ... */ });
// Route cache for production (cached to stockage/cache/routes.php)
$router->enableCache();Controller
Base controller:
namespace App\Controllers;
use App\Core\Controller;
class MyController extends Controller
{
public function index()
{
return $this->view('template', ['data' => $data]);
}
}Model
Base model:
namespace App\Models;
class MyModel
{
public static function find($id)
{
// Load from storage
}
public static function create($data)
{
// Create new record
}
}Coding Standards
PSR Standards
Follow PSR standards:
- PSR-1 - Basic coding standard
- PSR-4 - Autoloading standard
- PSR-12 - Extended coding style
Code Style
<?php
namespace App\Controllers;
use App\Core\Controller;
use App\Models\User;
class UserController extends Controller
{
public function index()
{
$users = User::all();
return $this->view('users.index', ['users' => $users]);
}
public function show($id)
{
$user = User::find($id);
if (!$user) {
return $this->notFound();
}
return $this->view('users.show', ['user' => $user]);
}
}Naming Conventions
- Classes: PascalCase -
UserController - Methods: camelCase -
getUserData() - Variables: camelCase -
$userData - Constants: UPPER_SNAKE_CASE -
MAX_FILE_SIZE - Files: Match class name -
UserController.php
Plugin Development
Plugin Structure
plugins/my-plugin/
âââ plugin.json
âââ MyPluginPlugin.php
âââ assets/
âââ views/
âââ README.mdPlugin Class
<?php
namespace App\Plugins\MyPlugin;
use App\Core\Plugin;
class MyPluginPlugin
{
public function boot()
{
// Register plugin routes
Plugin::hook('router.plugins.register', [$this, 'registerRoutes']);
// Add CSS to page header
Plugin::hook('view.header.styles', [$this, 'addStyles']);
}
public function registerRoutes($router)
{
$router->get('/my-plugin', function() {
return 'Hello from plugin!';
});
}
public function addStyles(&$styles)
{
$styles[] = \App\Helpers\PluginAssetHelper::loadCss('my-plugin', 'css/style.css');
}
}Available Hooks
Below are the most commonly used hooks for plugin development:
| Hook | Use case |
|---|---|
router.plugins.register | Register plugin routes (preferred for plugins) |
app.routes.register | Register application-level routes |
view.header.styles | Inject CSS into <head> |
view.footer.scripts | Inject JS before </body> |
view.footer.content | Inject HTML into footer |
view.navbar.items | Add items to the main navigation bar |
view.admin.sidebar.items | Add items to the admin sidebar |
admin.dashboard.widgets | Add widgets to the admin dashboard |
discussion.created | After a discussion is saved |
post.created | After a reply is saved |
user.registered | After a user account is created |
search.results | Filter or augment search results |
notification.before.create | Intercept notifications before they are written |
markdown.editor.config | Modify the Markdown editor configuration |
visitor.page_info | Resolve page info for unknown URLs (presence) |
presence.users | Filter/enrich active users list |
Theme Development
Theme Structure
themes/my-theme/
âââ theme.json
âââ assets/
â âââ css/
â âââ js/
â âââ img/
âââ views/Template Overrides
Override default templates:
themes/my-theme/views/
âââ layouts/
â âââ main.php
âââ discussions/
âââ list.phpCSS Variables
Use CSS variables for customization:
:root {
--primary-color: #007bff;
--secondary-color: #6c757d;
--background-color: #ffffff;
--text-color: #212529;
}Update Endpoint for Plugins and Themes
Since 5.3.7, Flatboard can check for updates on any plugin or theme that declares an update_url in its plugin.json or theme.json. When an update is available, it appears in Admin Panel > Tools > Updates alongside the core update, with the installed version, the latest available version, and a link to the changelog.
Declaring an update_url
Add the update_url field at the root of your plugin.json (or theme.json):
{
"name": "My Plugin",
"id": "my-plugin",
"version": "1.2.0",
"update_url": "https://example.com/api/my-plugin/version",
...
}The value can be:
- An absolute URL â used directly (
https://example.com/api/...) - A relative path â prefixed with the forum's
update_check_urlconfig value (e.g.api/plugins/my-pluginâhttps://versions.flatboard.org/api/plugins/my-plugin)
If the URL is relative and no update_check_url is configured, the check is silently skipped.
Required API response format
Your endpoint must return a JSON object with at least a version field:
{
"version": "1.3.0",
"changelog_url": "https://example.com/my-plugin/changelog"
}| Field | Required | Description |
|---|---|---|
version | Yes | Latest available version string (compared against plugin.json version) |
changelog_url | No | URL to the changelog or release notes, shown as a link in the updates page |
The check is performed via a simple GET request (cURL). Results are cached for 1 hour â no need to worry about hammering the endpoint.
update_url does not point to the official Flatboard registry (versions.flatboard.org) are flagged with a warning in the admin updates page. This is expected for community plugins hosted on personal or third-party servers.Minimal server-side example (PHP)
<?php
header('Content-Type: application/json');
echo json_encode([
'version' => '1.3.0',
'changelog_url' => 'https://example.com/my-plugin/releases/1.3.0',
]);Plugin Settings API
Plugin settings live in the "plugin" section of plugin.json. Always use Plugin::getData/setData/saveData â never Config::get/set â for plugin-specific values:
use App\Core\Plugin;
// Read a setting (third arg is default value)
$apiKey = Plugin::getData('my-plugin', 'api_key', '');
// Dot-notation for nested keys
$host = Plugin::getData('my-plugin', 'smtp.host', 'localhost');
// Write a setting (in-memory only)
Plugin::setData('my-plugin', 'api_key', 'abc123');
// Persist all settings to plugin.json
Plugin::saveData('my-plugin', ['api_key' => 'abc123', 'enabled' => true]);
// Get plugin stats (for monitoring)
$stats = Plugin::getStats();
// Returns: ['total' => int, 'active' => int, 'inactive' => int, 'hooks' => int]Presence Service
App\Services\PresenceService provides a unified API for querying who is currently on the forum (all methods are static):
use App\Services\PresenceService;
// All presence data (anonymous visitors + bots + logged-in users)
$all = PresenceService::getAllPresence(minutes: 15, includeBots: true);
// Returns: ['visitors' => [...], 'bots' => [...], 'users' => [...], 'all' => [...], 'stats' => [...]]
// Presence on a specific page
$page = PresenceService::getPresenceByPage('/d/123', minutes: 15);
// Aggregate stats only
$stats = PresenceService::getPresenceStats(minutes: 15);
// Returns: ['total' => int, 'anonymous' => int, 'authenticated' => int, 'bots' => int]
// Filter helpers (work on any presence array)
$filtered = PresenceService::filterByPageType($all['all'], 'discussion');
$filtered = PresenceService::filterByCategory($all['all'], 'general');
$filtered = PresenceService::filterByUserGroup($all['users'], 'moderator');
// Sorting
$sorted = PresenceService::sortPresence($all['all'], sortBy: 'last_activity', order: 'desc');VisitorTrackingMiddleware runs automatically on every non-AJAX HTML request. It skips: authenticated users, paths under /api/, /presence/update, /favicon.ico, /robots.txt, static file extensions. It fires visitor.before_track before writing each record.
Translation System
Global helper
// Both are equivalent
$text = Translator::trans('key', ['var' => 'value'], 'domain');
$text = __('key', ['var' => 'value'], 'domain');Advanced methods
// Get current language code
$lang = Translator::getLanguage(); // e.g., 'fr', 'en'
// Change language for the current request
Translator::setLanguage('en');
// Reload all translations from disk
Translator::reload();
// Reload only theme translation overrides
Translator::reloadThemeTranslations();
// Get all keys for a domain (useful for debugging)
$all = Translator::getAll('main');
// Register plugin translations programmatically
Translator::addPluginTranslations('my-plugin', ['key' => 'value']);Storage Development
Two storage APIs serve different purposes. Choose the right one for your use case.
StorageFactory â Core Flatboard Data
Use StorageFactory::create() to read or write core Flatboard entities (users, discussions, posts, categoriesâŠ). It returns the active StorageInterface implementation â JsonStorage on Community, SqliteStorage on Pro â so the same plugin code works on both editions without any change.
use App\Storage\StorageFactory;
$storage = StorageFactory::create();
// Examples of StorageInterface methods
$user = $storage->getUserById($userId);
$discussions = $storage->getDiscussionsByCategory($categoryId);
$post = $storage->getPostById($postId);JsonStorage or SqliteStorage directly in a plugin. Always go through StorageFactory::create() so your plugin works on both Community and Pro installations.AtomicFileHelper â Plugin Custom Data
Use AtomicFileHelper when your plugin needs to store its own data files (not Flatboard core entities). It provides atomic read/write operations backed by file locking â never use file_get_contents / file_put_contents directly.
Plugin data is typically stored inside the plugin's own directory, under a data/ subfolder. Use Plugin::getPath() to resolve the path safely regardless of the plugin's installation location:
use App\Core\AtomicFileHelper;
use App\Core\Plugin;
$dataDir = Plugin::getPath('my-plugin') . '/data';
$dataFile = $dataDir . '/records.json';
// Read plugin data (returns array or null if file absent)
$data = AtomicFileHelper::readAtomic($dataFile);
// Write plugin data (returns bool)
AtomicFileHelper::writeAtomic($dataFile, $data);
// Batch read multiple files in one pass
$results = AtomicFileHelper::readAtomicBatch([
$dataDir . '/records.json',
$dataDir . '/settings.json',
]);BASE_PATH . '/stockage/json/<your-plugin>/' instead, and handle cleanup in your uninstall() method.StorageFactory | AtomicFileHelper | |
|---|---|---|
| Purpose | Core Flatboard data (users, discussionsâŠ) | Plugin-specific custom files |
| Community | â (returns JsonStorage) | â |
| Pro | â (returns SqliteStorage) | â |
| Backend-agnostic | Yes â same API on both editions | N/A (JSON files only) |
| Survives uninstall | Yes (core data) | Only if stored in stockage/ |
Security Best Practices
Input Validation
Always validate input:
use App\Core\Validator;
// Pass request data to the constructor
$validator = new Validator($this->request->all());
$validator->required('email')->email('email');
$validator->required('username')->min('username', 3)->max('username', 30);
if (!$validator->isValid()) {
$errors = $validator->getErrors(); // ['field' => 'error message', ...]
Session::flash('errors', $errors);
$this->redirect(\App\Helpers\UrlHelper::to('/register'));
return;
}Output Sanitization
Sanitize all output:
use App\Core\Sanitizer;
// Strip dangerous HTML, keep safe tags (for rich content)
$clean = Sanitizer::sanitizeHtml($userInput);
// Strip all HTML tags (for plain text fields)
$clean = Sanitizer::sanitizeText($userInput);
// Escape for HTML output
echo Sanitizer::escape($value);
// Escape for use in an HTML attribute
echo Sanitizer::sanitizeForAttribute($value);CSRF Protection
Use CSRF tokens:
use App\Core\Csrf;
// Generate a token for the current session
$token = Csrf::token();
// Render a hidden input field (shortcut for use in views)
echo Csrf::field(); // <input type="hidden" name="csrf_token" value="...">
// Validate the token submitted with a form or API request
if (!Csrf::validate($token)) {
return $this->error('Invalid CSRF token');
}Testing
Unit Tests
Write unit tests:
use PHPUnit\Framework\TestCase;
class UserTest extends TestCase
{
public function testUserCreation()
{
$user = User::create([
'username' => 'testuser',
'email' => 'test@example.com'
]);
$this->assertNotNull($user);
}
}Integration Tests
Test integrations:
public function testApiEndpoint()
{
$response = $this->get('/api/discussions');
$this->assertEquals(200, $response->getStatusCode());
}Performance
Caching
Use caching:
use App\Core\Cache;
// Set cache
Cache::set('key', $data, 3600);
// Get cache
$data = Cache::get('key');
// Clear cache
Cache::clear('key');Database Optimization
Optimize queries:
// Use indexes
// Limit results
// Avoid N+1 queries
// Use transactionsContributing
Code Contribution
- Fork Repository - Fork on GitHub
- Create Branch - Create feature branch
- Write Code - Follow coding standards
- Test - Write and run tests
- Submit PR - Submit pull request
Documentation
- Code Comments - Add helpful comments
- PHPDoc - Document functions and classes
- README - Update README if needed
- Changelog - Update changelog
Version Compatibility
When developing plugins, themes, or customizations:
- Target Flatboard 5 - Code for Flatboard 5 architecture
- Not Compatible with v3/v4 - Code won't work on older versions
- Use Modern PHP - Take advantage of PHP 8 features
- Follow Architecture - Follow Flatboard 5 patterns
Resources
- Plugin Guide - Plugin development
- Theme Guide - Theme development
- API Documentation - API development
- GitHub Repository - Source code
Last updated: March 28, 2026