DEV Community

Daniel Baldwin
Daniel Baldwin

Posted on

TrueFramework v4.1 Updates

TrueFramework is a small, opinionated PHP framework built around three rules: plain PHP and plain files, one way to do common things, and pluggable where it matters. The v4.1 release focuses on the pieces every real-world app eventually needs — passkey login, expiring tokens, scheduled background work, sane error pages, and a one-class full featured email sender to keep the framework from bloat.

Here's what's new.

Passkey login with vendored WebAuthn / FIDO2
TrueFramework now ships passkey login support directly in the core package. The widely-used lbuchs/webauthn library is vendored under True\WebAuthn (its original namespace is preserved), so there's no extra Composer dependency to manage and no detached library to keep up to date alongside the framework. The standard registration and assertion flows — challenge issuance, attestation verification, signature validation — are all available out of the box.

The intended pairing is with TrueFramework's existing True\AuthenticationJWT class: a successful WebAuthn assertion calls the new loginUser($userId) helper, which mints a signed token and writes a hardened cookie. Plain password login continues to work for users who haven't enrolled a key.

Hardened JWT authentication
The True\AuthenticationJWT class has been tightened across the board:

Expiration is mandatory. Every issued token now carries an exp claim, and tokens without one are rejected. JWTs no longer live forever.
RS256 only. The legacy SHA1 / HS256 fallback paths have been removed. Tokens are signed with an RSA private key and verified with the matching public key.
Strict cookies. The auth cookie is written with SameSite=Strict, HttpOnly, and Secure attributes.
Real logout. logout() deletes the cookie with explicit attributes so it actually leaves the browser instead of being re-issued by the next request.
New loginUser($userId) helper. Issues a session for an arbitrary user from controller code — the foundation for WebAuthn and any other non-password login path.
TaskScheduler — cron-driven background work
The new True\TaskScheduler class lets you schedule one-shot or recurring tasks against a SQLite store, then run anything due from a single cron tick:

$scheduler = new True\TaskScheduler(BP.'/app/data/tasks.sqlite');
$scheduler->passObjects(['App' => $App]);

// Run a script once at a specific time
$scheduler->addTask('send-welcome.php', ['userId' => 42], [
'runAt' => '2026-05-12 09:00:00',
'name' => 'Welcome email for user 42',
]);

// Run a script every five minutes, forever
$scheduler->addTask('poll-feeds.php', [], [
'intervalSeconds' => 300,
'name' => 'Poll RSS feeds',
]);
A bundled runner at vendor/truecastdesign/true/workers/scheduledTaskRunner.php is invoked once a minute by cron. It pulls due tasks, runs each in its own scope with whatever objects you passed in via passObjects(), reschedules recurring ones, deletes successful one-shots, and records failures with the exception message. Stuck-in-running tasks older than five minutes are automatically released, so a crashed worker can't permanently jam the queue.

Pretty exception handling for PHP 8.5
Uncaught exceptions and PHP fatals no longer produce the wall of white and black text default output. A new handler in vendor/truecastdesign/true/src/Exceptions.php registers itself via set_exception_handler and register_shutdown_function, and renders a clean HTML page with the message, file, line, a code snippet around the offending line (with the failed line highlighted), and the stack trace.

The handler is loaded before init.php, so even parse errors inside init.php get a readable error page instead of a blank screen. True\App re-registers it after construction so its own exception handler doesn't replace the pretty one. The output includes an HTTP 500 status code and clears any pending output buffers — so partially-rendered pages don't leak into the error response.

A fluent Email class
The rewritten True\Email class (now at v1.7) replaces a multi-step setup with a single fluent chain. Connection, credentials, recipients, content, and headers are all configured by chained calls; send() returns a boolean and getLogs() exposes the SMTP transcript for debugging:

$mail = new \True\Email('smtp.domain.com', 587, 'tls', 'plain');

$ok = $mail->setLogin('user@domain.com', 'password')
->setFrom('user@domain.com', 'Acme')
->setReplyTo('support@domain.com', 'Support')
->addTo('customer@example.com', 'Jane Customer')
->setSubject('Welcome, {{first name}}!')
->setHtmlMessage('Hi {{first name}}, thanks for signing up.')
->setTextMessage('Hi {{first name}}, thanks for signing up.')
->setMessageReplaceVariables(['{{first name}}' => 'Jane'])
->addAttachment(BP.'/app/data/welcome.pdf')
->addDKIM(BP.'/app/data/dkim.private', 'domain.com')
->send();

if (!$ok) print_r($mail->getLogs());
Built-in features include:

SMTP authentication with plain, login, or cram-md5
TLS and SSL transport
HTML + plain-text multipart bodies
File attachments
Per-recipient merge variables via setMessageReplaceVariables()
Template variables for HTML bodies via setHTMLMessageVariables()
Optional DKIM signing
Configurable charset (defaults to UTF-8)
Custom headers, including X-Auto-Response-Suppress for transactional mail
Connection / response timeouts and per-message socket reuse are handled internally, so the same $mail object can send to multiple recipients without re-authenticating.

The rest of the bundled stack
TrueFramework's scaffold (truecastdesign/truefw) wires up two companion libraries that round out the batteries-included story:

Hopper — a thin PDO wrapper for MySQL and SQLite with a get($sql, $params, $type) shorthand that picks the return shape ('object', 'objects', 'array', 'arrays', 'list', 'value') so single-column lookups don't need post-processing.
Welder — a fluent form builder with validation, anti-spam, and CSRF tokens issued per-form and bound to the session.
Both are separate packages — projects that want Doctrine / Eloquent / Symfony Forms can ignore them entirely.

Routing, request, and response — unchanged on purpose
The router is still a flat list of HTTP-verb-prefixed lines in app/routes.php. Placeholders, route groups, and middleware are all there, but the syntax hasn't changed. Existing apps upgrade to v4.1 by bumping truecastdesign/true in composer.json — no controller rewrites, no router migration, no compiled cache to clear.

Try it
The fastest way to spin up a project is the scaffold:

composer create-project truecastdesign/truefw my-project
That gives you a working app with True, Hopper, and Welder pre-wired, the public document root in public_html/, and a sample route to start from.

Documentation lives at /docs. The project is on Packagist as truecastdesign/true, truecastdesign/hopper, and truecastdesign/welder.

Top comments (0)