Architecture
Folder structure
.htaccess Rewrites every request to index.php
index.php Front controller — defines APP_ROUTED, calls route()
config/
database.php Creates the $pdo connection
routes.php URL -> page file + allowed roles
roles.php List of all roles (value => label), used in dropdowns
.htaccess Blocks direct browser access to this folder
app/
bootstrap.php Central include: DB, session, helpers, auth functions.
Also blocks direct access to any page file.
helpers.php url(), redirectTo(), flash message helpers, BASE_PATH
auth.php Login, registration, roles, profile update functions
router.php Matches the request path against routes.php
.htaccess Blocks direct browser access to this folder
includes/
header.php <head>, navigation, flash message display
footer.php Closing tags
.htaccess Blocks direct browser access to this folder
pages/
home.php, login.php, register.php, logout.php,
dashboard.php, profile.php, role1_area.php,
admin_users.php, admin_user_new.php,
admin_user_edit.php, admin_user_delete.php
.htaccess Blocks direct browser access to this folder
sql/
database.sql Single SQL file: table + first-admin instructions
.htaccess Blocks direct browser access to this folder
tools/
generate_hash.php Dev helper to generate password_hash() values
Request flow
.htaccessrewrites every request that isn't an existing file or directory toindex.php.index.phpdefines theAPP_ROUTEDconstant (used later as a safeguard) and callsroute().app/router.php:- Loads
config/routes.php. - Reads
$_SERVER['REQUEST_URI']and strips theBASE_PATHprefix (see below) so the app works in any subfolder. - Looks up the resulting path in the routes table.
- If the route requires a login and the user isn't logged in,
redirects to
/login. - If the route restricts access to specific roles and the current user doesn't have one of them, returns a 403 page.
- Otherwise,
requires the matching file frompages/.
- Loads
- The page file (e.g.
pages/dashboard.php) renders the actual HTML, usingincludes/header.phpandincludes/footer.phpfor the shared layout.
Base-path detection (subfolder support)
The project can be placed in any subfolder of htdocs — you don't
need to hardcode a path anywhere. This is handled in
app/helpers.php:
$scriptDir = str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME'] ?? ''));
define('BASE_PATH', $scriptDir === '/' ? '' : rtrim($scriptDir, '/'));
SCRIPT_NAME always points at index.php (because every request is
rewritten there), so dirname() reliably yields the subfolder the
project lives in — e.g. /my-app when installed at
htdocs/my-app, or an empty string when installed directly in
htdocs.
Two things depend on BASE_PATH:
app/router.phpstrips it from the incoming request path before comparing it againstconfig/routes.php(which always uses paths like/login, without any folder prefix).- The
url()helper (app/helpers.php) prepends it to every generated link, redirect, and formaction. Always useurl('/some-path')instead of hardcoding/some-pathin page files — this is what makes renaming the project folder safe.
Why some pages use global $pdo;
Page files are required from inside the route() function in
app/router.php. PHP scopes variables per function, so a variable
declared in the global scope (like $pdo, created in
config/database.php) is not automatically visible inside a
required file that runs in a function's local scope.
Helper functions in app/auth.php (e.g. attemptLogin(),
findUserById()) already declare global $pdo; internally, so most
pages never need to worry about this — they just call the helper
functions. The four admin pages that run raw queries directly
(admin_users.php, admin_user_new.php, admin_user_edit.php,
admin_user_delete.php) declare global $pdo; explicitly at the top
for that reason.
Rule of thumb: if you add a new page that queries the database
directly instead of going through an app/auth.php helper, add
global $pdo; right after the bootstrap.php require.
Defense against direct file access
Two independent layers prevent someone from bypassing the router by
requesting a page file's real path directly (e.g.
/pages/admin_users.php), which would skip all login/role checks:
.htaccessfiles withRequire all deniedinapp/,config/,includes/,pages/, andsql/— Apache refuses to serve any file in these folders directly.- The
APP_ROUTEDconstant, defined only inindex.phpbefore the router runs.app/bootstrap.phpchecks for it and exits with a 403 if it's missing — a fallback in case the.htaccessrules are ever misconfigured or disabled.