Documenta o padrão atual (páginas = controller+vista, config/functions_*.php como models, sem front controller próprio) como decisão mantida, não herança silenciosa do CodeIgniter — mais auth partilhado, trash, CSRF, audit, multi-ginásio, platform-tools, deploy e Composer. Já bloqueado por HTTP (.htaccess já negava CLAUDE.md por nome). Inclui uma secção "What's not done" honesta: sem front controller real, sem DataTables, Bootstrap 4+5 em simultâneo — para não passar a imagem de que isto já está resolvido. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
6.5 KiB
fitme3070 — architecture
Backoffice + public site for a gym (ginásio) chain, plain PHP 8.4, no framework. This file exists because the pattern below was inherited from a CodeIgniter 4 port and had never been written down as a decision (audit 2026-09-24, item 6.1) — it describes what the codebase actually does, kept deliberately, not a design proposal for a rewrite.
Request lifecycle
Every page is its own controller-and-view combined. A page in views/admin/*.php
or site/*.php does, in order: bootstrap (config/db.php + config/functions.php),
an auth guard, read $_POST/$_GET and call model functions, then fall through into
its own HTML. There is no separate template layer and no front controller — 71+ PHP
files are directly reachable by URL (docroot is the repo root; .htaccess denies
partials, shared/platform-tools/*.php, and *.md, but doesn't allowlist).
This is a known, accepted gap, not an oversight — see "What's not done" below.
Introducing a real front controller/router for the app's own pages would touch most of
views/, .htaccess's friendly-URL block, and every hardcoded base_url('views/...')
call; it's a genuine rewrite, tracked separately, not bundled into this doc.
Data layer: config/functions_*.php as models
SQL lives in config/functions_*.php, one file per domain (functions_team.php,
functions_galerias.php, functions_gyms.php, functions_memberships.php, …) plus
functions_db.php for cross-cutting users queries. Functions are named
db_<noun>_<verb>() (db_gyms_create, db_team_update, db_gallery_list). Views
never write SQL directly — if you're adding a query, it goes in the matching
functions_*.php, not inline in the page. (Audit 6.3, closed 2026-09-25 — grep -rl '\$pdo->\(query\|prepare\|exec\)' views/ should always return nothing.)
config/functions.php is the loader: every page requires config/db.php (opens
$pdo, starts the session, connects to platform-tools) then
config/functions.php (requires every functions_*.php in order). Read that file's
own header comment before adding a new one — order matters for a few (auth.php
before anything that calls require_login(), etc.).
Auth
Login/session/roles run on shared/platform-tools/functions_auth.php ('auth' => 'shared' in config/db.php, migrated 2026-09-25 — audit 1.3). config/auth.php keeps
the app's own guard names (require_login(), require_admin(), require_staff(),
require_sysadmin(), is_admin(), etc.) as a thin layer over the shared primitives, so
none of the ~30 call sites in views/ had to change. Role ladder:
sysadmin(100) > admin(80) > staff(40) > client(10, portal-only). "Manter sessão
iniciada" is the shared auth_remember_* module.
The login-attempt throttle (config/functions_throttle.php) stays the app's own,
deliberately not the shared one — see the comment in config/db.php for why (avoids
double-counting the same form with two independent throttles). A throttled login feeds
platform_ip_record_offence(), so repeat offenders get fleet-wide IP-banned.
Soft delete / trash
Deletes are soft (deleted_at/deleted_by) via trash_delete() in
config/functions_trash.php. Gestão → Lixeira lists everything deleted across entity
types and only allows permanent purge after 30 days (enforced in the DELETE's
WHERE, not just hidden in the UI). Adding a new deletable entity means adding it to
trash_types() in that file.
CSRF
csrf_check() (config/functions_csrf.php) refuses any non-POST request outright —
state changes must be POST forms carrying csrf_field()'s token. There is no PRG
(Post-Redirect-Get) convention beyond that; several admin pages still re-render the
form directly on POST rather than redirecting (audit flags this as inconsistent, not
yet unified — tracked separately, low priority).
Audit log
Models call audit_row() before a write and audit_change() after (diffs
before/after automatically); ad-hoc events elsewhere use audit('entity.verb', [...]).
Both come from shared/platform-tools via config/functions_audit.php. Read via
Sistema → Auditoria (platform.php?tool=audit).
Multi-gym
Since 2026-09-25, one install serves several physical gyms: the gyms table, one URL
folder per gym (/3070/, /<postal-code>/), content rows carry gym_id, and the
backoffice has an "a editar" gym switcher (admin_gym_id() /
admin_gym() in config/functions_gyms.php). / and /home 301 to the default gym.
An unpublished gym 404s publicly but previews for staff.
platform-tools
shared/platform-tools is a git submodule (pinned branch = main in
.gitmodules), not vendored — see PLATFORM_TOOLS.md for why it went vendored→submodule→
back, and don't repeat that mistake without reading it first. config/db.php wires it
via platform_connect(): shared auth (above), fleet IP-ban guard (runs on every
request), the sysadmin router (platform.php / api/platform.php — Backups,
Actualizações, Auditoria, IP bans, Geo, Origem das visitas, all one front controller,
just for those pages, not the app's own).
Deploy
Push to master → Gitea webhook → api/platform.php?endpoint=auto_update → the VPS
pulls, syncs the submodule, applies pending database/migration_*.sql files, done in
seconds. No staging branch; the VPS checkout is what's live. deploy.sh (on the
VPS, not in this repo) is the manual fallback. See PLATFORM_TOOLS.md.
Dependencies
composer.json/composer.lock are real (composer install works, composer audit/outdated are meaningful) — added 2026-09-25, matching the fotografia
platform's setup. vendor/PHPMailer/ is the one hand-vendored exception, gitignore-
allowlisted: shared/platform-tools/functions_mail.php hard-codes that exact path
fleet-wide, so Composer's own vendor/phpmailer/phpmailer/ exists in parallel but
isn't what actually gets require_once'd. See ASSETS.md for frontend asset versions
(two Bootstrap majors, duplicated jQuery — known debt, not yet unified).
What's not done (tracked, not silently missing)
The full audit and its live status live in a separate report, not in this repo
(CLAUDE_GITEA/audits/fitme3070-auditoria-2026-09-24.html on the maintainer's
machine). As of 2026-09-25: no real front controller (6.2, above), no unified
create/edit view pairs, no DataTables on backoffice listings, public site still on
Bootstrap 4 while the backoffice runs 5, no srcset/responsive images. None of these
are urgent; none should be "fixed" as a side effect of an unrelated change — each is
its own piece of work.