A lightweight PHP framework for building simple, scalable and stable websites. Born in 2012 and modernized for PHP 8.1+, it keeps the friendly page-per-folder model while fixing the old codebase's security, concurrency and robustness gaps.
A hosted example of this repository runs at https://webbycms.brandon.my — the same Bootstrap 5 sample site you get from this repo, served from Apache behind a Cloudflare tunnel. See HOSTING.md for the full deployment steps.
- Modern PHP core — PSR-4 namespaced kernel (
src/WebbyCMS/), PHP 8.1+, Composer autoloading,.envconfiguration. - Security fixes — the forgeable
webbycms_logincookie login and theparse_strglobal-injection were removed; CSRF tokens, session hardening and prepared statements were added. - Stability fixes — double
session_start(), themysql_error()fatal and leaked error pages are gone; errors are logged via Monolog. - Performance — anonymous page cache (filesystem) plus lazy sessions keep public pages cheap on one instance, with a path to Redis later.
- Bootstrap 5.3 — upgraded from Bootstrap 3; jQuery dependency dropped.
- Testing & CI — PHPUnit suite, PHPStan level 5, lint script, GitHub Actions matrix.
- Page-per-folder authoring — drop a
pages/my-page/index.phpfile in and it's live. - Clean routing —
/page,/page/cate,/page/cate/action/idand.htmlvariants. - Hardened sessions — strict mode, HttpOnly,
SameSite=Lax, secure-cookie on HTTPS, lazily started so anonymous traffic stays cache-friendly. - Auth API —
Password,AuthManager,Gateand per-session CSRF tokens (no login pages are forced on you). - Optional PDO database — prepared statements only, enabled via
.env. - Anonymous page cache — filesystem-backed, so public pages skip most PHP work.
- Proper errors & logs — PSR-3 logging (Monolog), safe 500 page, stack traces only in debug.
- Tested — PHPUnit suite + PHPStan level 5 + a lint script, run in CI.
- PHP >= 8.1 with
pdo(andpdo_sqlite/pdo_mysqlfor database use) - Apache +
mod_rewrite(or the PHP built-in server for development) - Composer
composer install
cp .env.example .env # then edit APP_URL, APP_DEBUG, etc.php -S 0.0.0.0:8000 router.phpPoint your vhost docroot at the project root and make sure .htaccess overrides
are allowed (AllowOverride All). /page/cate/action/id URLs are rewritten to
index.php.
docker build -t webbycms .
docker run -p 8080:80 webbycmsOpen http://localhost:8080 — you should see the Bootstrap 5 sample page.
index.php front controller
router.php dev router for `php -S`
src/WebbyCMS/ the framework kernel
Bootstrap.php wires everything together
Config.php Request.php Response.php Session.php Flash.php Router.php View.php Logger.php
Auth/ Password, AuthManager, Csrf, Gate, UserProviderInterface
Cache/ CacheInterface, FilesystemCache
Db/ Database (PDO)
Error/ ErrorHandler
helpers.php global helpers (config, e, csrf_field, auth, ...)
pages/{page}/index.php your pages (each folder is a URL)
pages/{page}/forms/*.php POST handlers for that page
includes/ shared HTML head/body/footer templates
controller/ LEGACY shims (deprecated, see UPGRADE.md)
assets/ Bootstrap 5 CSS/JS + your custom files
storage/ logs, cache, sessions (git-ignored)
tests/ PHPUnit suite
Create pages/hello/index.php:
<div class="container my-5">
<h1>Hello <?= e(request()->input('name', 'world')) ?></h1>
<?php if (gate()->allows('admin')): ?>
<p>You are an admin.</p>
<?php endif; ?>
</div>Visit /hello and /hello?name=Webby.
A POST with a hidden form field routes to pages/{page}/forms/{form}.php:
<?php
// pages/home/forms/contact.php
$name = (string) $request->input('name', '');
if ($name === '') {
flash()->error('Name is required.');
} else {
flash()->success("Thanks, $name!");
}
return redirect('/');Add the hidden field and CSRF token to your form:
<form method="post">
<input type="hidden" name="form" value="contact">
<?= csrf_field() ?>
<input type="text" name="name">
<button>Send</button>
</form>All configuration lives in .env — copy .env.example and adjust:
| Variable | Purpose |
|---|---|
APP_ENV |
development / production |
APP_DEBUG |
show stack traces (dev only) |
APP_URL |
base URL used by the url() helper |
DB_ENABLED |
set true to use the PDO database layer |
SESSION_NAME |
session cookie name |
PAGE_CACHE_ENABLED |
anonymous page cache on/off |
PAGE_CACHE_TTL |
page cache lifetime in seconds |
CSRF_ENFORCE |
require a valid _token on all POSTs (enable after migrating forms) |
- Anonymous GET responses are cached to
storage/cacheand served without touching the session or database. - Sessions start only when the page actually needs them, so cached pages never create session files.
- The database layer uses prepared statements; the connection is opened lazily.
- To scale out later, point
SESSION_NAME/session storage at a shared store (e.g. Redis) without changing page code.
composer lint # php -l over src, pages, includes, controller
composer analyse # PHPStan level 5
composer test # PHPUnitSee UPGRADE.md.
GPL-3.0-or-later