1 Architecture
stak administrator edited this page 2026-09-10 11:42:25 +00:00

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

  1. .htaccess rewrites every request that isn't an existing file or directory to index.php.
  2. index.php defines the APP_ROUTED constant (used later as a safeguard) and calls route().
  3. app/router.php:
    • Loads config/routes.php.
    • Reads $_SERVER['REQUEST_URI'] and strips the BASE_PATH prefix (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 from pages/.
  4. The page file (e.g. pages/dashboard.php) renders the actual HTML, using includes/header.php and includes/footer.php for 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.php strips it from the incoming request path before comparing it against config/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 form action. Always use url('/some-path') instead of hardcoding /some-path in 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:

  1. .htaccess files with Require all denied in app/, config/, includes/, pages/, and sql/ — Apache refuses to serve any file in these folders directly.
  2. The APP_ROUTED constant, defined only in index.php before the router runs. app/bootstrap.php checks for it and exits with a 403 if it's missing — a fallback in case the .htaccess rules are ever misconfigured or disabled.