Prejsť na obsah

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

Must: ask for original values

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

Watch: crcCheck() once per request

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']);
    
Must: show HTTP 400 in the UI

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