MIT licensed · Version 2.0

MVCLM PHP

A small MVC framework in plain PHP, shipped with the application most projects rebuild from scratch. A new project starts from something that already runs.

PHP 8.1+ MySQL / MariaDB Tailwind CSS v4 Apache + mod_rewrite No packages

What Is Included

All of it working on the day you clone it, not scaffolded for you to finish.

Authentication

✅ Registration with server-side validation and a live username check
✅ Sign-in against hashed passwords, session and CSRF token rotated on success
✅ Remember me, a database-backed token in a 30 day cookie
✅ Sign-out is POST only and deletes the token row
✅ Wrong-password counter per IP address, with a 24 hour limit
✅ Account self-delete, POST only, behind a confirmation
✅ Optional email verification, single-use link expiring after 24 hours
✅ Optional password reset, single-use link expiring after an hour

Users and Access Levels

✅ Three access levels: user, moderator, admin
✅ Four account statuses: active, inactive, suspended, pending
✅ Suspended and inactive accounts cannot sign in, and are signed out on their next request
✅ Public profile pages, optionally restricted to signed-in visitors
✅ Suspended and inactive profiles answer 404, so the status stays private

Admin

✅ Users list with search, sorting and pagination
✅ User edit page for access level and status
✅ An administrator cannot demote themselves
✅ Analytics: growth and registration charts, breakdowns, key metrics

Security

✅ Every POST is checked for a CSRF token in the front controller, before routing
✅ Prepared-statement helpers on the database wrapper
✅ The source folder is denied to the web server by its own .htaccess
✅ The settings file, database password included, sits above the public root

Operations

✅ Demo mode blocks registration, deletion, access-level changes, password resets and contact submissions
✅ A developer error page and a production 500 page, switched by one setting
✅ Central error and exception handlers writing to a dated log
✅ Real 404 and 500 pages, and flash messages in four types
✅ A contact form with a honeypot and a Reply-To header
✅ One script creates the schema and seeds fifty four accounts

Getting Started

Requirements

PHP 8.1 or newer, with the mysqli and openssl extensions
MySQL or MariaDB
Apache with mod_rewrite and AllowOverride enabled
Composer

PHP 8.1 is a floor rather than a preference. The database wrapper relies on mysqli raising an exception when a call fails, which is the default from 8.1 onward. Both the routing and the deny over the source folder are .htaccess rules, so neither works without AllowOverride.

Installation

# Clone and generate the autoloader
git clone https://github.com/jaanus-saarnak/mvclm.git
cd mvclm
composer install --working-dir=www/src

# Fill in url and database credentials
cp env.example.php env.php

# Create the schema and seed the accounts
php migration.php

Point the web server's document root at www/ and sign in. The migration script is destructive and asks nothing first: it drops each of its five tables before creating them, and it rebuilds whichever database the settings file names.

composer install pulls no packages. It is there to generate the autoloader that the front controller requires on every request.

Worth Knowing Before You Build On It

The handful of things that will cost you an afternoon if nobody tells you.

A new form needs a CSRF field

The front controller refuses any POST without one, before routing, so the request never reaches a controller. A form that silently bounces with "That form has expired" is a missing hidden field, not an expired session.

A route matches the path, never the method

A state-changing route has to test for POST itself. The router will happily hand a GET to an action that deletes something.

Routes are matched in order

The profile route is registered last and accepts any single word as a username, so a route added after it is unreachable.

Demo mode is not a read-only switch

It guards five features by name. It cannot block writes in general, because signing in writes to the database. The reset and contact pages stay visible so the features can still be seen; only their submissions are refused.

Small On Purpose

Seven classes in the core. No service container, no configuration discovery, no code generation. Every request enters through one front controller and the routing table is a single readable file. Every front-end library is served from the repository at a pinned version, so no script can change under the application without a commit. The stylesheet ships built, which is why running it needs no Node.

License

MIT. The bundled third-party front-end code keeps its own terms, and everything bundled is MIT as well: jQuery, Chart.js and DataTables, each with its licence file beside it. The Inter webfont is deliberately not bundled, since it is under the SIL Open Font License rather than MIT, and is loaded from Google Fonts instead. Every page still renders without it.