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 classes

Directory 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/               # Dependencies

Core 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.md

Plugin 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:

HookUse case
router.plugins.registerRegister plugin routes (preferred for plugins)
app.routes.registerRegister application-level routes
view.header.stylesInject CSS into <head>
view.footer.scriptsInject JS before </body>
view.footer.contentInject HTML into footer
view.navbar.itemsAdd items to the main navigation bar
view.admin.sidebar.itemsAdd items to the admin sidebar
admin.dashboard.widgetsAdd widgets to the admin dashboard
discussion.createdAfter a discussion is saved
post.createdAfter a reply is saved
user.registeredAfter a user account is created
search.resultsFilter or augment search results
notification.before.createIntercept notifications before they are written
markdown.editor.configModify the Markdown editor configuration
visitor.page_infoResolve page info for unknown URLs (presence)
presence.usersFilter/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.php

CSS Variables

Use CSS variables for customization:

:root {
  --primary-color: #007bff;
  --secondary-color: #6c757d;
  --background-color: #ffffff;
  --text-color: #212529;
}

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

JSON Storage

Working with JSON storage:

use App\Core\AtomicFileHelper;

// Read (returns array or null if file absent)
$data = AtomicFileHelper::readAtomic('path/to/file.json');

// Write (returns bool)
AtomicFileHelper::writeAtomic('path/to/file.json', $data);

// Batch read multiple files in one pass
$results = AtomicFileHelper::readAtomicBatch([
    'path/to/file1.json',
    'path/to/file2.json',
]);

SQLite Storage (Pro)

Working with SQLite:

use App\Storage\SqliteStorage;

$storage = new SqliteStorage();
$result = $storage->query('SELECT * FROM users WHERE id = ?', [$id]);

Security Best Practices

Input Validation

Always validate input:

use App\Core\Validator;

$validator = new Validator();
$validator->required('email')->email('email');
if (!$validator->validate($data)) {
    return $this->error($validator->errors());
}

Output Sanitization

Sanitize all output:

use App\Core\Sanitizer;

$clean = Sanitizer::clean($userInput);
echo htmlspecialchars($clean, ENT_QUOTES, 'UTF-8');

CSRF Protection

Use CSRF tokens:

use App\Core\Csrf;

// Generate token
$token = Csrf::token();

// Verify token
if (!Csrf::verify($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' => '[email protected]'
        ]);
        $this->assertNotNull($user);
    }
}

Integration Tests

Test integrations:

public function testApiEndpoint()
{
    $response = $this->get('/api/v1/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 transactions

Contributing

Code Contribution

  1. Fork Repository - Fork on GitHub
  2. Create Branch - Create feature branch
  3. Write Code - Follow coding standards
  4. Test - Write and run tests
  5. 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

Resources

Last updated: February 23, 2026