Architecture
Manager System (backend classes in lib/)
- PluginManager — handles plugin discovery, enable/disable, hook registration/execution (actions, filters, checks)
- ThemeManager — handles theme discovery, activation, and CSS URL/path resolution
- UpdateManager — handles version tracking, update checks, and backup/recovery for core, plugins, and themes
- Migrator — file-based database migrations with locking, transactional execution, and batch rollbacks
- AuthZ — centralized authorization service (role-based permissions, ownership checks,
can:permissionmiddleware)
All managers are instantiated in index.php (after the bootstrap) and are fully integrated into the routing layer and admin panel. The Migrator is also used by the CLI (bb.php).
Data Layer (lib/DbQuery.php)
Lightweight query builder over PDO/BbPdo. No ORM, no magic — just sugar over prepared statements:
$db = new DbQuery($pdo);
$user = $db->table('users')->where('id', 42)->first();
$threads = $db->table('threads')->where('status', 'visible')->orderBy('created_at', 'DESC')->limit(10)->get();
$db->table('users')->insert(['username' => 'alice', 'email' => 'a@b.com']);
$db->table('users')->where('id', 42)->update(['email' => 'new@b.com']);
$db->table('users')->where('status', 'banned')->delete();
$page = $db->table('threads')->where('category_id', 5)->paginate(15, $page);
Supports SQLite and MySQL transparently via BbPdo. All core data operations use this layer.
Rendering Layer (src/Renderer.php)
Micro template renderer that separates logic from presentation:
$r = new Bulletin\Renderer(__DIR__ . '/views');
$r->display('thread-clean', ['thread' => $thread, 'posts' => $posts]);
Template helpers: $this->e() (escaped output), $this->partial(), $this->renderComponent(), $this->slot()/$this->yield() (layouts), $this->csrfField(), $this->when(), $this->each().
Layered Structure (no framework, zero dependencies)
The application is still a single upload with no Composer, no Docker, no build step — but the old single index.php has been split into small, focused files under src/:
index.php— the thin front controller. It wires the bootstrap, database, managers, registers all routes and dispatches the request throughBulletin\Router.src/Security.php— security helpers: CSRF protection, rate limiting, input validation, security loggingsrc/Response.php—Bulletin\Response— HTTP response value object withhtml(),json(),redirect(),error()factory methodssrc/Errors.php— typed HTTP exceptions (UnauthorizedException,ForbiddenException,NotFoundException,ValidationException,ConflictException,TooManyRequestsException,MethodNotAllowedException)src/bootstrap.php— install check,config.jsonload, i18n setup, PSR-4 autoloader (delegates toTrustedProxies.phpandsession_setup.php)src/helpers.php— loads helper modules fromsrc/Helpers/and remaining helpers (base_url(),redirect())src/Helpers/Url.php— URL generation (url(),slugify(),current_route_action())src/Helpers/AuthHelpers.php— auth helpers (is_logged_in(),is_admin(),can_view_thread(),validate_password_strength()). Permission checks go through theAuthZservice; the legacyuser_has_permission()helper was removed in 0.9.0.src/Helpers/Upload.php— upload validation (validate_upload(),get_uploaded_images())src/Helpers/Mail.php— email sending (send_email()via SMTP or PHP mail)src/Helpers/Notifications.php— notification helpers (notify_thread_reply(),notify_admin_new_user(),notify_mentioned_users(),create_notification())src/Helpers/Text.php— text/content helpers (escape(),validate_input(),clean_text(),time_ago(),compact_number(),excerpt(),marked_parse())src/Helpers/Avatar.php— avatar rendering (avatar_initial(),avatar_color(),render_avatar())src/Helpers/Data.php— data fetching (sidebar_categories(),forum_statistics(),thread_sort_options(),fetch_threads())src/TrustedProxies.php— trusted proxy detection for correct client IP behind reverse proxiessrc/session_setup.php— session configuration and hardeningsrc/Router.php—Bulletin\Router— middleware-enabled request router with route groups, named parameters, middleware pipeline,can:permission middleware, and top-levelHttpExceptioncatching.src/Request.php—Bulletin\Request— centralized input sanitization (get/post/input/has/raw) with typed getters (string/int/bool/email/enum).src/Renderer.php—Bulletin\Renderer— micro template engine for clean view rendering.src/setup.php— ensures directories exist and initialises the database (SQLite/MySQL schema, defaults).src/actions/— split action handlers:admin.php— dispatcher that includes modular admin handlers:admin/settings.php— site settings, SMTP, image uploadadmin/moderation.php— thread/post moderation, split/mergeadmin/users.php— user management, roles, create/edit/deleteadmin/categories.php— category CRUD and orderingadmin/langs.php— language managementadmin/diagnostics.php— system diagnosticsadmin/plugins.php— plugin managementadmin/themes.php— theme managementadmin/catalog.php— extension catalogadmin/updates.php— core/extension updates
posts.php— dispatcher for post/thread actions (loadsposts-thread.php,posts-new.php,posts-edit.php)posts-thread.php— thread view, watch, unwatch, image uploadposts-new.php— new thread creationposts-edit.php— reply, edit post, delete post, edit thread, delete threadusers.php— login, register, profile, password resetcontent.php— categories, search, downloadmisc.php— markdown preview, mention autocomplete
Key traits that remain unchanged:
- SQLite by default, MySQL configurable via
config.json - Session-based authentication
- SEO-friendly URLs via
.htaccessrewrite rules
SEO-Friendly URLs
Clean URLs are supported via .htaccess rewrite rules. All requests are routed to index.php where the Bulletin\Router matches the path against registered routes:
/thread/{id}-{slug}— single thread view/category/{id}-{slug}— category view/u/{username}— user profile/admin/*— admin panel routes
Routing with Middleware (src/Router.php)
The Bulletin\Router class handles all request dispatch through a middleware pipeline. Routes are registered in index.php with get(), post(), etc., and organized into groups with middleware.
The router supports automatic content negotiation — requests with Accept: application/json or paths under /api/* receive JSON responses automatically.
Middleware Mode Example
use Bulletin\Router;
$router = new Router();
// Apply middleware to a group
$router->middleware('auth')->group(function($router) {
$router->get('/thread/{id:\d+}', fn($p) => handle_thread($p['id']));
$router->post('/thread/{id:\d+}/reply', fn($p) => handle_reply($p['id']));
});
// API routes
$router->api()->middleware('auth', 'csrf')->post('/api/threads', 'api_create_thread');
// Custom middleware
$router->registerMiddleware('rate_limit', function($params) {
if (rate_limited()) return ['status' => 429, 'body' => 'Too many requests'];
return null;
});
$router->dispatch();
API Routes
The router automatically detects JSON requests and sets the appropriate Content-Type header:
// Automatic JSON response for API routes
$router->api()->get('/api/threads', function($params) {
return ['threads' => fetch_threads()]; // Auto-encoded to JSON
});
// Or detect via Accept header
$router->get('/data', function($params) {
return ['key' => 'value']; // JSON if Accept: application/json
});
| Middleware | Purpose |
|---|---|
guest |
Redirect logged-in users away (for login/register pages) |
auth |
Require authentication, redirect to login if missing |
admin |
Require admin.access permission via AuthZ, 403 if unauthorized |
csrf |
Validate CSRF token on POST requests |
can:permission |
Require a specific permission via AuthZ (e.g., can:moderation.manage) |
Route Parameters
Named parameters with optional type constraints:
$router->get('/thread/{id:\d+}', $handler); // digits only
$router->get('/user/{name}', $handler); // any non-slash
$router->get('/post/{slug:[a-z0-9-]+}', $handler); // custom regex
Directory Structure
/bulletinbored/
├── config.json # Configuration (database, email, site, theme, localization)
├── index.php # Thin front controller (bootstrap + routing only)
├── bb.php # CLI entry point (migrate, plugin:list, cache:flush, ...)
├── router.php # Router for PHP built-in server (dev)
├── .htaccess # SEO-friendly URL rewrites (Apache/LiteSpeed)
├── nginx.conf # Nginx server block with rewrite rules and deny rules
├── web.config # IIS URL Rewrite rules and deny rules
├── VERSION # Single-source-of-truth version file
├── migrations/ # File-based database migrations (namespaced IDs: core:, plugin:)
│ └── YYYYMMDD_description.php
├── lib/ # Backend managers and data layer
│ ├── BbPdo.php # PDO wrapper with SQLite/MySQL SQL normalization
│ ├── DbQuery.php # Lightweight query builder (table/where/first/insert/update/delete)
│ ├── Migrator.php # File-based migration engine (up/down, batches, rollback)
│ ├── PluginManager.php # Plugin manager facade (composed from the traits below)
│ ├── PluginManager/ # Cohesive traits: PluginHooks, PluginManifest, PluginDependencies, PluginPackages
│ ├── PluginDiscovery.php # Plugin folder/legacy-file discovery and manifest parsing
│ ├── PackageInstaller.php # ZIP extraction (Zip Slip safe), flattening, integrity checks
│ ├── ThemeManager.php # Theme discovery, activation
│ ├── UpdateManager.php # Version tracking, updates, backup/recovery
│ ├── AuthZ.php # Authorization service (role-based permissions, ownership)
│ └── repo_install.php # Repository-based install/upgrade helpers
├── src/ # Application core (no framework)
│ ├── bootstrap.php # install check, config, i18n, PSR-4 autoloader
│ ├── Security.php # CSRF, rate limiting, input validation, security logging
│ ├── Response.php # Bulletin\Response — HTTP response value object
│ ├── Errors.php # Typed HTTP exceptions
│ ├── TrustedProxies.php # Trusted proxy detection (IPv4/IPv6/CIDR)
│ ├── session_setup.php # Session configuration and hardening
│ ├── App.php # ApplicationContext — centralized state (replaces $GLOBALS)
│ ├── helpers.php # loads helper modules from src/Helpers/
│ ├── Helpers/ # modular helper functions
│ │ ├── Url.php # URL generation, slugify, current_route_action
│ │ ├── AuthHelpers.php # auth helpers, can_view_thread, validate_password_strength
│ │ ├── Upload.php # upload validation, get_uploaded_images
│ │ ├── Mail.php # send_email (SMTP + PHP mail)
│ │ ├── Notifications.php # notifications and admin alerts
│ │ ├── Text.php # escape, validate_input, time_ago, excerpt, marked_parse
│ │ ├── Avatar.php # avatar_initial, avatar_color, render_avatar
│ │ └── Data.php # sidebar_categories, forum_statistics, fetch_threads
│ ├── Router.php # Bulletin\Router — middleware-enabled request router
│ ├── Request.php # Bulletin\Request — centralized input sanitization
│ ├── Renderer.php # Bulletin\Renderer — micro template engine
│ ├── setup.php # directory checks + database initialisation (via Migrator)
│ ├── actions/ # split action handlers
│ │ ├── admin.php # admin panel, settings, updates, catalog
│ │ ├── posts.php # dispatcher (loads posts-thread, posts-new, posts-edit)
│ │ ├── posts-thread.php # thread view, watch, unwatch, image upload
│ │ ├── posts-new.php # new thread creation
│ │ ├── posts-edit.php # reply, edit, delete post/thread
│ │ ├── users.php # login, register, profile, password reset
│ │ ├── content.php # categories, search, download
│ │ └── misc.php # markdown preview, mention autocomplete
├── tests/ # Zero-dependency test suite (346 core test functions / 1242 assertions)
│ ├── harness.php # Test + TestSuite classes (the engine)
│ ├── run.php # CLI runner
│ ├── DbQueryTest.php # Query builder tests
│ ├── E2eFlowTest.php # End-to-end flow tests (thread lifecycle, JSON API, plugins)
│ ├── PluginManagerTest.php # Hook system + manifest validation tests
│ ├── AuthTest.php # Auth, permissions, CSRF, AuthZ tests
│ ├── MigratorTest.php # Migration engine tests
│ ├── SecurityTest.php # CSRF rotation, Request, audit log, trusted proxies tests
│ ├── ResponseTest.php # Response object + typed Request tests
│ ├── MarkdownTest.php # Markdown security tests (XSS, URL schemes)
│ └── PluginRouterTest.php # Plugin route/middleware registration tests
├── views/ # Template files
│ ├── header.php # Shared frontend header/footer (loads theme CSS)
│ ├── thread-clean.php # Clean thread view using components
│ ├── components/ # Reusable UI components
│ │ ├── post.php # Single post component
│ │ └── thread_modals.php # Moderation modals
│ ├── partials/ # Legacy partials (sidebar, thread_list)
│ └── ... # Other view templates
├── themes/ # Theme system (like plugins)
│ └── freshbored/
│ └── style.css # Default theme styles
├── plugins/ # Plugin system
├── uploads/ # File upload storage (auto-created)
│ └── avatars/ # User avatar uploads
├── data/ # SQLite database storage (auto-created)
│ ├── .htaccess # blocks direct access to database/config files
│ ├── logs/ # Admin audit log (security.log)
│ └── ratelimit/ # Rate limiter buckets (auto-created)
├── lang/ # Localization files (JSON only — never PHP)
│ └── en.json # English translations (add your own <code>.json</code> here)
└── README.md
Theme System
Themes work like plugins — each theme is a folder in themes/ with a style.css file:
- Configure active theme in
config.json:"theme": "freshbored" - Create custom themes by adding folders in
themes/ - Theme CSS is automatically loaded by
views/header.php - All frontend pages use the active theme
- Admin panel uses Bootstrap 5 default styles
Plugin System
Plugins are PHP files in plugins/ that define an {name}_init() function:
$pluginManager->addHook('event', $callback)— register a hook$pluginManager->runHook('event', ...$args)— fire a hook- Plugins are auto-loaded on every request
Example plugin in plugins/hellobored/hellobored.php demonstrates the pattern.