Custom PHP MVC auth system (SecureAuth). PHP >= 8.2, MySQL/MariaDB, no build step — Apache serves public/ directly. Full file-by-file reference: CLAUDE.md (read it for architecture details); this file is the lean "things you'd get wrong" list.
- Served at
http://localhost/Encriptacion_PHP/public(path under Apache docroot via XAMPPhtdocs/), not a subfolder served as webroot. Requires XAMPP Apache + MySQL up. - No build/compile step, no npm. Do not scaffold
package.json/node_modulesinto the project for tooling or e2e. - All asset/URL paths use the
APP_URLconstant (<?= APP_URL ?>).
- Do not run
phpunitbefore requiring: create DBlogin_test, importdatabase/schema_test.sql, and have a.env.testingwithDB_DATABASE=login_test. composer test(all),composer test:unit,composer test:integration(phpunit.xml suitesUnit/Integration).- Integration tests hit real MySQL — never mock
\mysqli.tests/TestCase.phpthrows ifDB_DATABASE === 'login'(protects prod DB). - Never load
app/Config/autoload.phpin tests — it starts a session, reads cookies, and connects the DB singleton.tests/bootstrap.phpinstead fills$_ENVfrom.env.testingviaparse_ini_fileBEFORE requiringvendor/autoload.php(because Composer'sautoload.filesrunsapp/Config/config.php). It also explicitlyrequirescache.php(not inautoload.files) and connects\mysqlidirectly. - Timezone-sensitive date comparisons in SQL use
DATE_SUB(NOW(), INTERVAL X HOUR)— never PHP-computed timestamps (PHP/MySQL timezone drift).
- PSR-4:
App\→app/,App\Lib\→libs/(note: maps tolibs/, not a subpath ofapp/),Tests\→tests/. - Composer
autoload.filesauto-loadsapp/Config/config.php(loads.env, definesenv()andAPP_URL).app/Config/autoload.phpis the runtime bootstrap (cache + DB + secure session +Auth::restoreFromCookie()) — do not treat it as config-only. - Routes:
routes/web.php— each URL maps to[Controller::class, 'method']; dispatch on HTTP method + path inapp/Core/Router.php.
- Mutations (delete, logout, revoke, session end) are POST-only and carry a CSRF token;
Csrf::verify()useshash_equals()and rotates the token after each success. - POST detection uses
isset($_POST['btnXXX'])— hidden<button type="submit">(novalue) submits an empty string that!empty()would misread. - Auth views (
views/auth/) are standalone with their own<head>, load onlymain.js+main2.js(no jQuery/Bootstrap JS), andsweetalert2.all.min.jsloads only when a flash message exists (inviews/layouts/messages.php). Auth copy is English by design (Spanish dashboard is fine). - Security headers are in
public/.htaccess(mod_headers) — modify there. - Cache: file-backed (
libs/Cache/FileCache.php), keyusers.all,CACHE_ENABLED=falsein.env.testingand forced byphpunit.xml. - Flash notifications travel via
$_SESSION['message']+$_SESSION['icon'], never URL params. - Old-input retention on validation errors (
UserController::create()/edit()): non-password fields go in$_SESSION['old']before the redirect, read once andunset()on the next GET — same one-shot pattern as flash messages.
- Use the
playwright-cliskill (globally installed, browsers in~/.cache/ms-playwright):playwright-cli open --browser=firefox --headed http://localhost/Encriptacion_PHP/public/login. System Brave Origin via CDP (attach --cdp=http://localhost:9222); system Firefox cannot be automated (Playwright needs its own build, already installed).