AI blog · DotApp PHP Framework 2.0
Request lifecycle in DotApp PHP Framework
After boot, every HTTP hit is a RequestObj passed into the controller as $request.
Incoming GET/POST is auto-protected so a forgotten sanitizer does not open XSS / injection-era holes.
That is why Request has an explicit switch: data() is the escaped copy; data(true) is the original.
This article is the map: how the body is collected, when to ask for original values, query() vs matchData(),
crcCheck(), form(), and why HTTP 400 never reaches form .after().
Request data: protected vs original
The framework protects the programmer by default. You must explicitly ask when you need the payload as the client sent it.
$request->data() (or data(false)) — protected, escaped copy. Safe to print into HTML.
$request->data(true) — original array. Passwords, decrypt, hashes, compare, persist, CRC unwrap.
After a secure-channel unwrap, fields live under $request->data(true)['data'].
Characters such as ), =, %, quotes, and & are rewritten in the protected copy.
Hashing that copy is hashing a different password.
DotApp::DotApp()->unprotect($variable) still exists for a value you already hold (by reference). Prefer data(true) on the request.
Common mistakes
| Wrong | Right |
|---|---|
$request->data()['data']['password'] into Auth::login |
$request->data(true)['data']['password'] |
Read $request->id for {id} in the URL |
$request->matchData()['id'] |
Treat GET query as data() |
$request->query() (and query(true) for original GET) |
Call $request->headers() |
That method does not exist |
Only .after() on a form; HTTP 400 shows a blank page |
Hook .onError(), parseReply the error body, unstick loaders |
Skip crcCheck() then trust the body |
Always crcCheck() first on channel posts — once |
Global middleware / Router::before calls crcCheck(), then the handler calls it again |
One call per request. A passing check burns the one-time token. Second call is false |
What arrives on the request
The constructor parses the body once and caches it. GET uses $_GET. POST uses $_POST, falling back to JSON or parse_str on php://input.
PUT / PATCH / DELETE parse the raw body the same way. HEAD / OPTIONS are empty.
| Call | Returns | Use |
|---|---|---|
data() |
Protected array (by reference) | Print into HTML |
data(true) |
Original array | Secrets, decrypt, persist, unwrap |
data(true)['data'] |
Channel fields after unwrap | Every fo-rm / load() / Bridge payload |
query() / query(true) |
GET array | Query string. Same protect switch as data() |
matchData() |
Route params | {id:i}, optional segments — not POST body |
getMethod() / getPath() |
Lowercase method / path | Disallowed method → 405 and exit |
There is no headers() method. Uploads use $request->upload() — not crcCheck() on that endpoint.
Full forms: How to create secure forms.
Auth passwords: Authentication and 2FA.
Channel posts
A passing crcCheck() invalidates the one-time CSRF token in the posted envelope.
The result is not cached. Call it again on the same request (global middleware / Router::before
and the handler) and the second call returns false — used token — HTTP 400 —
even though the first check passed.
Pick one place. Default: the handler. If a before-hook already ran it, the handler must not.
form() does not run CRC again.
Middleware detail: Middleware — crcCheck once.
Browser Shop posts ride /assets/dotapp/dotapp.js: { data, crc } plus header dotapp: load.
PHP: crcCheck() first. Then form(['POST'], 'saveContact', ok, err) for a named form, or read
$request->data(true)['data'] for a load() / Bridge payload.
Always provide the form error callback. A mismatch without it throws. Handler / URL mismatch returns null. Method mismatch returns false.
if (!$request->crcCheck()) {
return DotApp::DotApp()->ajaxReply(['status' => 0, 'message' => 'Bad request'], 400);
}
$answer = $request->form(['POST'], 'saveContact', function ($request) {
$data = $request->data(true)['data'] ?? [];
$email = (string) ($data['email'] ?? '');
// persist with named RAW — values in the second argument
return ['code' => 200, 'body' => ['status' => 1, 'message' => 'Saved']];
}, function () {
return ['code' => 403, 'body' => ['status' => 0, 'message' => 'Invalid signature']];
});
if (!is_array($answer) || !isset($answer['body'])) {
return DotApp::DotApp()->ajaxReply(['status' => 0, 'message' => 'Rejected'], 400);
}
return DotApp::DotApp()->ajaxReply($answer['body'], $answer['code']);
crcCheck failure and a rejected envelope are HTTP 400/403. $dotapp().load() then calls the error callback / form .onError() — not .after().
The body is still ajaxReply. parseReply it, show message, unstick loaders.
Application errors (validation, wrong password) should stay HTTP 200 with status 0 so .after() can show them.
Client contract: How to use $dotapp() JavaScript.
What the controller returns
| Return | Effect |
|---|---|
| HTML string | Becomes the response body |
new Response($code, $body) |
Short-circuits the pipeline |
Response::json($array, $code) |
Ordinary JSON HTTP (not the browser channel) |
Response::redirect($url, 302) |
Redirect |
DotApp::DotApp()->ajaxReply($body, $code) |
Base64 JSON — client parseReply |
Channel UI uses ajaxReply. Public JSON APIs use Router + Response::json.
Routing: How routing works.
Complete Shop save handler
File: app/modules/Shop/Controllers/Contact.php — the POST half.
Read original fields, validate, persist with named RAW, answer with ajaxReply.
<?php
namespace Dotsystems\App\Modules\Shop\Controllers;
use Dotsystems\App\DotApp;
use Dotsystems\App\Parts\DB;
use Dotsystems\App\Parts\Logger;
use Dotsystems\App\Parts\Validator;
class Contact extends \Dotsystems\App\Parts\Controller
{
public static function save($request)
{
if (!$request->crcCheck()) {
return DotApp::DotApp()->ajaxReply(['status' => 0, 'message' => 'Bad request'], 400);
}
$answer = $request->form(
['POST'],
'saveContact',
function ($request) {
$data = $request->data(true)['data'] ?? [];
$email = trim((string) ($data['email'] ?? ''));
$message = (string) ($data['message'] ?? '');
if (!Validator::isEmail($email) || $message === '') {
return ['code' => 200, 'body' => [
'status' => 0, 'message' => 'Enter a valid email and a message',
]];
}
DB::module('RAW')->q(function ($qb) use ($email, $message) {
$qb->raw(
'INSERT INTO shop_contacts (email, message, created_at)
VALUES (:email, :message, :created_at)',
[
'email' => $email,
'message' => $message,
'created_at' => date('Y-m-d H:i:s'),
]
);
})->execute(
function () {},
function ($error) { Logger::use()->error('contact save failed', $error); }
);
return ['code' => 200, 'body' => ['status' => 1, 'message' => 'Saved']];
},
function () {
return ['code' => 403, 'body' => ['status' => 0, 'message' => 'Invalid signature']];
}
);
if (!is_array($answer) || !isset($answer['body'])) {
return DotApp::DotApp()->ajaxReply(['status' => 0, 'message' => 'Rejected'], 400);
}
return DotApp::DotApp()->ajaxReply($answer['body'], $answer['code']);
}
}
FAQ
Why auto-protect at all?
The framework was built to keep the programmer from one forgotten escape. Values are escaped on the way in. You print the protected copy. You ask for original data when the bytes must stay literal.
Do special characters in a password break login?
Not if you use data(true). The protected copy rewrites ), =, %, quotes, and &.
Installer / CLI Auth::createUser does not run Request protect — it hashes whatever string arrived.
If you passed the password as a shell argument, the shell may eat those characters.
Why did “Bad request” show nothing?
HTTP 400/403 never reach .after(). Hook .onError() and parseReply the body.
Is query() protected too?
Yes. query() is the escaped GET bag. query(true) is original GET. Route params stay in matchData().
When do I call unprotect()?
When you already hold a variable that went through protect and you do not have the request switch.
Prefer $request->data(true). Call DotApp::DotApp()->unprotect($var) by reference — do not reassign the return value.
Middleware crcCheck then the handler crcCheck — why 400?
The first call burned the one-time token. The second call is a used-token failure.
Call crcCheck() in exactly one place. See crcCheck once.
ajaxReply or Response::json?
Browser channel: ajaxReply + parseReply. Ordinary HTTP JSON: Response::json.
See also
- Middleware in DotApp PHP Framework
- How to create secure forms in DotApp PHP Framework
- How authentication and 2FA work in DotApp PHP Framework
- How routing works in DotApp PHP Framework
- How to use $dotapp() JavaScript in DotApp PHP Framework
- Controllers and Response in DotApp PHP Framework
- How to build JSON endpoints with Router in DotApp PHP Framework
- How translations and i18n work in DotApp PHP Framework
- Official documentation