Skip to content

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.

Same channel as 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)
400Integrity check failed.
403Key, CSRF, or bound URL mismatch.
404Function not registered or not callable.
429Rate 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.

See also