AI blog · DotApp PHP Framework 2.0
How to call PHP from JavaScript with DotBridge
DotBridge is how a Shop page calls a named PHP function from a click, keyup, or similar discrete action — without wrapping that control in <fo-rm>.
You register the function with Bridge::listen or $dotApp->bridge->fn, put {{ dotbridge:on(click)="…" }} on the control, and keep /assets/dotapp/dotapp.js on the page.
The POST uses the same secure channel as $dotapp().load(). PHP still checks rights.
Common mistakes
| Wrong | Right |
|---|---|
| Put a contact form, login, or multi-field save on Bridge. | Prefer <fo-rm> plus {{ formName(...) }} for full forms. Bridge is for discrete actions. |
Skip /assets/dotapp/dotapp.js or copy a file from app/parts/js/. |
Load the generated URL. Do not edit app/parts/js/. |
| Trust the click because the template attribute is present. | The PHP function still runs Auth::can() / ownership checks and validates the payload. |
| Register dozens of one-off Bridge buttons that never fire. | Fewer, parameterised functions. Session storage of listeners is capped (default 200, oldest dropped). |
Call a DotApp endpoint with raw fetch. |
Let the attribute fire Bridge, or use $dotapp().load() for one-shot list actions. |
When to use Bridge
Use Bridge when the user triggers a named PHP function: ping an email, refresh a badge, run a small staff action.
Use <fo-rm> when they fill several fields and submit.
Use $dotapp().load() for pager, toggle, delete, and reorder (encrypted data-*, no Bridge attribute required).
Files go through $dotapp().uploadFile, not Bridge and not load() as FormData.
load()
Bridge rides the generated dotapp.js transport (integrity check, CSRF binding, encrypted function name).
Do not invent a second AJAX stack.
Mechanism: How DotApp PHP Framework protects the browser-to-PHP channel.
Register the PHP function
Do this in initialize($dotApp) or early in the controller that renders the page.
The return value becomes the JSON body. A non-callable handler throws \Exception.
Success is HTTP 200 with status: 1 and header X-Answered-By: dotbridge.
<?php
use Dotsystems\App\Parts\Auth;
use Dotsystems\App\Parts\Bridge;
use Dotsystems\App\Parts\Router;
$dotApp->bridge->fn('ping', function ($request) {
if (!Auth::can(['Shop.staff'])) {
return ['ok' => false, 'message' => 'Forbidden'];
}
$email = $request->data(true)['data']['email'] ?? '';
if ($email === '') {
return ['ok' => false, 'message' => 'Missing email'];
}
return ['ok' => true, 'email' => $email];
})
->before(function ($request) { /* optional extra check */ })
->after(function ($request) { /* optional log */ });
Bridge::listen binds the same function to an explicit URL (useful when the attribute uses url(/shop/bridge)):
Bridge::listen(
Config::module('Shop', 'prefix') . '/bridge',
'ping',
function ($request) {
if (!Auth::can(['Shop.staff'])) {
return ['ok' => false, 'message' => 'Forbidden'];
}
$email = $request->data(true)['data']['email'] ?? '';
return ['ok' => true, 'email' => $email];
},
Router::STATIC_ROUTE
);
| API | Use |
|---|---|
$dotApp->bridge->fn('ping', $cb) |
Register on the current page’s Bridge. Chain before / after. |
Bridge::listen($url, 'ping', $cb, $static) |
Register and bind to a POST URL. Pass Router::STATIC_ROUTE for an exact path. |
Bridge::addFilter($name, $callback) |
Add a named input filter for dotbridge:input. |
Template on(click) attribute
Mark the input with dotbridge="email" so the function argument email maps to that field.
Put public options on the same {{ dotbridge:on(...) }} tag — they are markup flags, not something you re-implement in PHP crypto.
<input type="text" dotbridge="email" />
<button type="button"
{{ dotbridge:on(click)="ping(email)" regenerateId oneTimeUse rateLimit(60,10) }}>
Ping
</button>
<script src="/assets/dotapp/dotapp.js"></script>
| Option | Meaning | Default |
|---|---|---|
dotbridge:on(event)="fn(a,b)" |
Call PHP fn on a DOM event. |
— |
dotbridge:on(event,key)="fn()" |
For example on(keyup,Enter). |
— |
dotbridge:input="filter.name(args)" |
Live input filter attributes. | — |
regenerateId |
Rotate the request id after each call. | off |
oneTimeUse |
Single use (also applies a long 1-call window). | off |
rateLimit(seconds,count) |
Repeatable rate limit, e.g. rateLimit(60,10). |
none |
internalID(id) |
Stable session storage key. | hash of the function name |
expireAt(timestamp) |
Absolute expiry unix time. | 0 (disabled) |
url(path) |
Custom POST target (bound and verified). | current route |
Built-in dotbridge:input filters: email, url, phone, password, date, time, creditcard, username, ipv4.
Add your own with Bridge::addFilter($name, $callback).
Optional JavaScript hooks
The attribute is enough to fire PHP. Use $dotapp().bridge('ping','click') when you need to block, toast, or handle HTTP codes without alert().
(function () {
var runMe = function ($dotapp) {
$dotapp()
.bridge('ping', 'click')
.before(function (data, el) {})
.onValueError(function (inputName, el) {})
.after(function (body, el) {
if (body && body.ok) {
$dotapp('#shopStatus').attr('hide', 'false').text('Ping sent');
} else if (body && body.message) {
$dotapp('#shopStatus').attr('hide', 'false').text(body.message);
}
})
.onResponseCode(function (fn, code) {
$dotapp('#shopStatus').attr('hide', 'false').text('Request failed');
}, 429);
};
if (window.$dotapp) runMe(window.$dotapp);
else window.addEventListener('dotapp', function () { runMe(window.$dotapp); }, { once: true });
})();
| HTTP | Typical cause (high level) |
|---|---|
| 400 | Integrity check failed. |
| 403 | Key, CSRF, or bound URL mismatch. |
| 404 | Function not registered or not callable. |
| 429 | Rate limit, invalid id, or missing valid key. |
Complete Shop ping page
View: app/modules/Shop/views/ping.view.php. Pair it with the fn('ping') registration above.
Keep rights in PHP even when the button is hidden in CSS.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{{ var: $title }}</title>
</head>
<body>
<h1>{{ var: $title }}</h1>
<p id="shopStatus" hide="true"></p>
<label for="shopEmail">Email</label>
<input id="shopEmail" type="text" dotbridge="email"
{{ dotbridge:input="email" }} />
<button type="button"
{{ dotbridge:on(click)="ping(email)" regenerateId oneTimeUse rateLimit(60,10) }}>
Ping
</button>
<script src="/assets/dotapp/dotapp.js"></script>
<script src="/assets/modules/Shop/js/ping.js"></script>
</body>
</html>
FAQ
Can Bridge replace every form?
No. A profile save, login, or CMS article is a <fo-rm>. Bridge is the click-sized cousin of load().
Does rateLimit(60,10) replace Auth::can()?
No. Rate limits are a public throttle on that control. Authorization still happens in the PHP function.
Do I have to write the .bridge() hooks?
No. The template attribute calls PHP by itself. Hooks are for UX: toasts, extra client checks, handling 429.
Why did an old Bridge button stop working?
Config::bridge('storage_limit') defaults to 200 listeners per session. Prefer fewer functions with parameters. Override the cap with Bridge::max_keys($n) only when you must.
How do I validate the email before PHP runs?
Use {{ dotbridge:input="email" }} (or email.address(...) with your own arguments) on the input. PHP still validates the string it receives.