AI blog · DotApp PHP Framework 2.0
Secure backend-frontend communication in DotApp PHP Framework
Shop UI talks to PHP on one encrypted, session-bound channel. You still have to pick the right surface:
a real form, a one-shot load(), a named Bridge function, or uploadFile.
PHP always crcCheck() (except the upload endpoint), then rights.
Mechanism: How DotApp PHP Framework protects the browser-to-PHP channel.
Forms: How to create secure forms in DotApp PHP Framework.
Common mistakes
| Wrong | Right |
|---|---|
Wrap every table-row click in fo-rm “for security”. |
fo-rm is several fields plus Submit. Clicks use $dotapp().load() on the same channel. |
Put a file or ZIP in FormData on load() or inside fo-rm. |
$dotapp().uploadFile(file, url, progress). CRC cannot wrap a file. |
Raw fetch to a Shop POST, or skip crcCheck(). |
load() / hijacked fo-rm / Bridge. PHP still crcCheck() then Auth::can(). |
alert() / window.confirm() before delete; leave the list clickable while load() runs. |
Graphical confirm in the Shop module. Overlay the region until success and error. |
Raw ids on data-*; location.reload() after a toggle. |
Encrypted ids with a unique extra key per field. Patch reply.html. Pager: AJAX lists. |
Decision tree
| Use | When |
|---|---|
fo-rm + formName |
User fills several fields and submits |
$dotapp().load() |
Click, toggle, delete, paginate, filter, reorder / drag-and-drop |
| Bridge | Discrete named PHP functions from JS / template on(click) |
uploadFile |
Files / ZIP — never FormData on load() / fo-rm because CRC cannot wrap a file |
A fo-rm submit is hijacked into the same load() pipeline.
fo-rm does not make a click safer than load(). It makes a multi-field submit bind a handler name, action URL, and method via formName.
Client boot: How to use $dotapp() JavaScript.
Named functions: How to call PHP from JavaScript with DotBridge.
PHP always crcCheck() then rights
Every Shop mutation on the channel follows the same order. Frontend overlays are UX only.
$request->crcCheck()— fail means 400 and no field reads.- Decrypt identifiers with the same extra key you used in the template.
false→ stop. Auth::can()and ownership. Encryption is not authorization.- Validate, persist, return
{ status, message, html }viaajaxReply.
Two layers, both required:
| Layer | What it protects | What it does not |
|---|---|---|
fo-rm / load() + crcCheck |
Transport (tamper, CSRF, wrong handler) | Meaning of an id |
| Unique extra key on every frontend identifier | Cross-field mix-up (Shop.user.id versus Shop.product.id) |
Rights / ownership — still Auth::can() |
Markup: data-item="{{ enc(Shop.item.id): $item.id }}".
JS reads the attribute and posts it. PHP decrypts with 'Shop.item.id'.
Do not put a second identifier on the same extra key. Do not skip rights because the value looks opaque.
Complete Shop toggle (load, not fo-rm)
One add/edit form may sit above the table. The table itself is buttons. Bind with .live() so replaced HTML still works.
Cover #listWrap while the request is in flight (desktop and mobile). Patch a child (#listInner), not the wrapper you overlay.
<div id="listWrap" class="shop_listwrap">
<div id="status" hide="hide"></div>
<table>
<tbody id="listInner">
<tr data-item="{{ enc(Shop.item.id): $item.id }}">
<td>{{ var: $item.title }}</td>
<td>
<button type="button" class="js-toggle">Toggle</button>
<button type="button" class="js-delete">Delete</button>
</td>
</tr>
</tbody>
</table>
</div>
(function () {
var listBusy = false;
var listDone = function () {
listBusy = false;
$dotapp("#listWrap").removeClass("shop_busy");
};
var runMe = function ($dotapp) {
$dotapp().live("click", ".js-toggle", function (e) {
if (listBusy) return;
listBusy = true;
$dotapp("#listWrap").addClass("shop_busy");
$dotapp().load("/shop/items/toggle", "POST", {
id: $dotapp(e.currentTarget).closest("tr").attr("data-item"),
f: "toggle"
}, function (raw) {
var reply = $dotapp().parseReply(raw);
if (reply && reply.status == 1 && reply.html) $dotapp("#listInner").html(reply.html);
if (reply && reply.message) $dotapp("#status").attr("hide", "false").html(reply.message);
listDone();
}, function () { listDone(); });
});
};
if (window.$dotapp) runMe(window.$dotapp);
else window.addEventListener("dotapp", function () { runMe(window.$dotapp); }, { once: true });
})();
<?php
public static function toggle($request)
{
// $tableHtml = re-rendered #listInner fragment (rows + pager)
if (!$request->crcCheck()) {
return DotApp::DotApp()->ajaxReply(['status' => 0, 'message' => 'Bad request'], 400);
}
$data = $request->data(true)['data'] ?? [];
$id = Crypto::decrypt($data['id'] ?? '', 'Shop.item.id');
if ($id === false) {
return DotApp::DotApp()->ajaxReply(['status' => 0, 'message' => 'Bad id'], 200);
}
if (!Auth::can('Shop.items.edit')) {
return DotApp::DotApp()->ajaxReply(['status' => 0, 'message' => 'Forbidden'], 200);
}
return DotApp::DotApp()->ajaxReply([
'status' => 1,
'message' => 'Updated',
'html' => $tableHtml,
], 200);
}
Overlay while in flight
Build the preloader in the Shop module (.shop_busy). Core does not ship a page overlay.
Show it before load(). Remove it in both the success and error callbacks.
Cover the region being mutated. Intercept pointer and touch. One in-flight request per region.
Patch the inner fragment so you do not wipe the overlay node.
Confirm UI before delete
Any Shop delete (row, detail, bulk) opens a graphical dialog first: title, what will be deleted, Cancel, and a destructive confirm.
Large enough buttons on a phone. Never alert(), window.confirm(), or prompt(), and never delete on the first click.
Put the item name with .text(), not .html(). Only after confirm: overlay + load() with the encrypted id.
Build the dialog in Shop assets. Core does not ship one.
$dotapp().live("click", ".js-delete", function (e) {
var id = $dotapp(e.currentTarget).closest("tr").attr("data-item");
$dotapp("#shopConfirmTitle").text("Delete this item?");
$dotapp("#shopConfirmText").text("This cannot be undone.");
$dotapp("#shopConfirm").removeAttr("hidden");
$dotapp("#shopConfirm .js-confirm-ok").off("click").on("click", function () {
$dotapp("#shopConfirm").attr("hidden", "hidden");
if (listBusy) return;
listBusy = true;
$dotapp("#listWrap").addClass("shop_busy");
$dotapp().load("/shop/items/delete", "POST", { id: id }, function (raw) {
var reply = $dotapp().parseReply(raw);
if (reply && reply.status == 1 && reply.html) $dotapp("#listInner").html(reply.html);
listDone();
}, function () { listDone(); });
});
$dotapp("#shopConfirm .js-confirm-cancel").off("click").on("click", function () {
$dotapp("#shopConfirm").attr("hidden", "hidden");
});
});
Bridge: named PHP from JS
Use Bridge when you need a discrete named PHP function — a ping, a small lookup — from JS or from template on(click).
It is not a replacement for a multi-field Shop form. It still rides the channel (CRC, session keys, Referer binding).
PHP registers $dotApp->bridge->fn('ping', …). Full modifiers and error codes:
How to call PHP from JavaScript with DotBridge.
uploadFile for files and ZIP
load() and fo-rm post JSON { data, crc }. A file stuffed into FormData on that pipeline cannot carry the CRC.
PHP crcCheck() fails and the user sees a generic error instead of your message.
Use $dotapp().uploadFile(file, url, progress), then parseReply on the text.
PHP uses $request->upload() — do not crcCheck() on that endpoint (there is no { data, crc }).
Guard with middleware / Auth::can(). Reject executables in PHP (extension plus finfo on the bytes). Frontend accept= is UX only.
JSON that is not the channel
Browser Shop actions on the channel return ajaxReply for parseReply.
If you need an ordinary JSON HTTP endpoint (not this browser channel), register it with Router and return Response::json.
Do not send channel posts as raw JSON with fetch. Do not mix ajaxReply and Response::json on the same action.
<?php
Router::get('/shop/catalog.json', 'Shop:Catalog@index!', Router::STATIC_ROUTE);
public static function index($request)
{
return Response::json(['status' => 1, 'items' => $rows], 200);
}
When the surface is a form
Several fields plus Submit: fo-rm, formName between the tags, generated dotapp.js, .form() with halt and loaders,
PHP crcCheck() then form(['POST'], 'saveContact', ok, err), then ajaxReply.
Complete files: How to create secure forms.
Lists that grow: pager and search via load(), not ?page= reloads —
How to build AJAX lists with pagination.
FAQ
Is a fo-rm around a delete button more secure than load()?
No. Both use CRC, transport CSRF, and header dotapp: load. A delete is one shot: button + encrypted data-* + load().
Wrapping it in fo-rm adds markup noise and invites a submit button in a table row. It does not add a stronger channel.
Why must I skip crcCheck on upload?
There is no { data, crc } body. Calling crcCheck() there always fails.
Authorize with Auth::can() (and middleware). Return HTTP 200 with status 0 or 1 so the Promise resolves and you can toast reply.message.
Why not window.confirm?
It is blocking, ugly on mobile, and easy to ignore in the product UX. Ship a Shop dialog with Cancel and a clear destructive action.
Only then call load().
What if I forget to clear the overlay on error?
The list stays blocked forever. That is a bug. Always listDone() in the error callback as well as after a successful parseReply.
Can one extra key cover every data-* on the row?
No. Unique extra key per identifier field. Shop.item.id on the row token, a different string if you also send a user id.
Then Auth::can() still runs.
Bridge or load() for “toggle this item”?
Prefer load() with an encrypted id and a small payload. Bridge is for discrete named functions you want to declare in the template
(on(click), rate limit, one-time use). Do not invent a Bridge function per table row if a single toggle URL will do.
See DotBridge.
When do I use Response::json?
Ordinary JSON HTTP routes that are not the browser channel: Router + Response::json.
Shop UI posts on the channel use ajaxReply + parseReply.
Do load() pages still need /assets/dotapp/dotapp.js?
Yes. The URL is generated per client and injects session key material. Without it, load(), forms, and Bridge fail.
Referer required or that route 404s — operational fact. Channel article: browser-to-PHP channel.
Can the pager be a form GET?
No. Accumulating Shop lists paginate with load(), overlay, and a patched fragment.
Do not reload with ?page=. Walkthrough: AJAX lists.
See also
- How DotApp PHP Framework protects the browser-to-PHP channel
- How to create secure forms in DotApp PHP Framework
- How to call PHP from JavaScript with DotBridge
- How to build AJAX lists with pagination in DotApp PHP Framework
- How to use $dotapp() JavaScript in DotApp PHP Framework
- Official documentation