name: php-js-ajax-writer description: Write jQuery AJAX caller functions in JS files and their matching PHP handler files. Use this skill whenever the user needs to wire up a frontend jQuery $.ajax() call to a backend PHP endpoint — even if they say things like "add a button that saves data", "fetch table data from PHP", "make this form submit via AJAX", "write the JS and PHP for this action", or "hook this up to the backend". Always trigger when the task involves both a JS side (button click, table load, form submit) and a PHP side (processing the request, querying the database, returning JSON). Also trigger when the user has only one side written and needs the other.
jQuery AJAX Handler Skill
Produces two artifacts for every task: a JS caller block and a PHP handler file. Both follow strict patterns defined below. Always write both unless the user explicitly asks for only one side.
This skill integrates the php-simplifier conventions for the PHP side — every PHP file produced must follow those rules (PHP 8.1 syntax, strategy dispatch, PHPDoc blocks, file-level comment). Read those rules in Section 4 before writing any PHP.
1. Identify the Trigger Type
Before writing any code, classify what initiates the AJAX call. This determines the UI loading/reset pattern used in the JS caller.
| Trigger type | Initiated by | Loading behaviour |
|---|---|---|
| Button | A button click | Disable button, swap label to spinner + "Processing…" text |
| Table load | Page load or a filter/tab | Replace <tbody> content with a full-width spinner row |
| Form submit | A form submit event | Disable submit button, swap label to spinner + "Saving…" text |
| Silent | Programmatic / polling | No visible UI change — errors shown in a toast or console only |
If the user's description is ambiguous, ask one short question to confirm the trigger type before proceeding.
2. JS Caller Block — Rules
2.1 Function skeleton
Every AJAX caller is a named function that accepts a payload object. It is never
an anonymous function or an inline $.ajax() call at the event-handler level. The
event handler (click, submit, change) calls this named function and passes a payload.
/**
* [One-line description of what this call does.]
*
* @param {Object} payload Keys sent to the PHP handler (must include `action`).
*/
function callSomething(payload) {
$.ajax({
url: ajaxUrl, // path to the PHP handler — defined as a const/var at file top
type: "POST",
dataType: "json",
data: payload,
beforeSend: function () {
/* see Section 2.2 */
},
success: function (res) {
/* see Section 2.3 */
},
error: function (xhr, status, err) {
/* see Section 2.4 */
},
complete: function () {
/* see Section 2.5 */
},
});
}
ajaxUrlis declared once at the top of the JS file as aconst. Never hardcode the path inside the function.payloadmust always include anactionkey matching a handler registered in the PHP dispatcher.
2.2 beforeSend — loading states
Button trigger:
beforeSend: function () {
$btn.prop('disabled', true)
.html('<i class="fa fa-spinner fa-spin me-1"></i> Processing…');
$errorEl.addClass('d-none').text('');
}
$btnis captured outside the function at the event-binding point:const $btn = $(this);inside the click handler, passed in or closed over.$errorElis the element designated to show error messages (e.g. an alert<div>).
Table load trigger:
beforeSend: function () {
const colSpan = $('#myTable thead tr th').length;
$('#myTable tbody').html(
`<tr><td colspan="${colSpan}" class="text-center py-3">
<i class="fa fa-spinner fa-spin me-1"></i> Loading data…
</td></tr>`
);
}
- Always derive
colspandynamically from the actual<thead>column count. Never hardcode it.
Form submit trigger: Same pattern as button, but also call e.preventDefault() in
the submit handler before calling the AJAX function.
Silent trigger: beforeSend is omitted or left empty.
2.3 success — handle the response
The PHP handler always returns { status: bool, data: mixed, message: string }.
success: function (res) {
if (res.status) {
// happy path — use res.data
} else {
// server-side failure — show res.message
$errorEl.removeClass('d-none').text(res.message);
}
}
- For table loads: rebuild
<tbody>fromres.datain the happy path. Ifres.datais empty, show a "No records found." row. - For button/form: show a success toast or update the relevant DOM element.
- Never use
.html()with raw server data — use.text()for all user-facing strings. Only use.html()when constructing structured markup from trusted, escaped data.
2.4 error — network or server failure
error: function (xhr, status, err) {
const msg = xhr.responseJSON?.message ?? `Request failed: ${err}`;
$errorEl.removeClass('d-none').text(msg);
}
- Always attempt to read
xhr.responseJSON.messagefirst (the PHP handler sets this even on 4xx/5xx responses). Fall back to the rawerrstring. - For table loads: replace
<tbody>with a single error row instead of using$errorEl.
2.5 complete — always reset UI
complete: function () {
$btn.prop('disabled', false).html(originalLabel); // button trigger
// — OR —
// nothing needed for table (tbody was replaced in success/error)
}
- Capture the button's original label before
beforeSendruns:const originalLabel = $btn.html();at the event-binding point. - For table triggers,
completeis typically empty — the tbody is already in its final state fromsuccessorerror.
2.6 Event binding (written outside the AJAX function)
$(document).ready(function () {
// Button example
$("#saveBtn").on("click", function () {
const $btn = $(this);
const originalLabel = $btn.html();
const payload = {
action: "save_record",
id: $("#recordId").val(),
name: $("#recordName").val().trim(),
};
callSomething(payload, $btn, originalLabel);
});
// Table load on page ready
loadTableData();
});
Pass $btn and originalLabel as extra parameters into the AJAX function when the
trigger is a button, so the function can reference them without relying on closures
that might capture stale values.
3. PHP Handler File — Rules
3.1 Required structure (in order)
<?php— no closing tag at end of file- File-level PHPDoc block (see Section 4)
header('Content-Type: application/json');- Session guard if the project uses sessions (require session_check, call login_required)
respond()helper function- Method enforcement (
$_SERVER['REQUEST_METHOD'] !== 'POST') - Handler functions (one per action)
dispatchAction()function using strategy array- Entry point (read
$_POST, extractaction, calldispatchAction)
3.2 respond() helper
/**
* ? Sends a JSON response and terminates execution.
*
* @param bool $status Whether the operation succeeded.
* @param mixed $data Payload to return to the client.
* @param string $message Human-readable status message.
* @param int $code HTTP status code.
*
* @return void
*/
function respond(bool $status, mixed $data = [], string $message = '', int $code = 200): void
{
http_response_code($code);
echo json_encode([
'status' => $status,
'data' => $data,
'message' => $message,
]);
exit;
}
3.3 Dispatcher pattern
Always use the strategy array dispatch pattern from the php-simplifier skill:
function dispatchAction(string $action, array $payload): void
{
$strategies = [
'action_key' => 'handlerFunctionName',
// ...
];
$handler = $strategies[$action] ?? null;
if ($handler === null) {
respond(false, [], "Invalid action: {$action}", 400);
}
$handler($payload);
}
3.4 Handler function conventions
- Name:
handle+ PascalCase action name. Example:action = 'get_students'→handleGetStudents. - Always validate required payload keys at the top of the handler using
?? nulland respond with 400 on missing input. - Use PHP 8.1 syntax throughout (match, null coalescing, named args, typed params).
- Every handler has a PHPDoc block following php-simplifier Rule 7.
3.5 Entry point
$payload = $_POST;
$action = $payload['action'] ?? null;
if (!$action) {
respond(false, [], 'No action provided.', 400);
}
dispatchAction($action, $payload);
4. PHP Code Quality — Use the php-simplifier Skill
Before writing any PHP file, read the php-simplifier skill and apply all of its rules to every PHP file produced by this skill. Do not skip this step.
The php-simplifier skill covers: PHP 8.1 syntax modernisation, null coalescing, strategy dispatch, redundant variable elimination, short array syntax, file-level PHPDoc blocks, and function-level PHPDoc blocks — all of which are mandatory here.
If the php-simplifier SKILL.md is available at
/mnt/skills/user/php-simplifier/SKILL.md(or the path shown inavailable_skills), read it with theviewtool before generating any PHP output.
5. Output Format
Always produce output in this order:
- A short paragraph confirming the trigger type identified and the action(s) being wired.
- JS block — fenced as
```js, with a comment at the top naming the file it belongs in (e.g.// logViewer.js). - PHP file — fenced as
```php, with a comment at the top naming the file (e.g.// admin-panel/_partials/ajax/log_viewer_operations.php). - A brief "What to wire up" note: any HTML attributes (IDs, data attributes) that the JS references and must exist in the template.
6. Worked Examples
Example A — Button trigger (delete a record)
User says: "Write the AJAX to delete a student when the Delete button is clicked.
The button is #deleteBtn and passes a student id."
JS output:
// students.js
const ajaxUrl = "ajax/students_operations.php";
/**
* Sends a delete request for a single student record.
*
* @param {Object} payload Must include `action` and `id`.
* @param {jQuery} $btn The button that triggered the call.
* @param {string} originalLabel The button's original HTML label.
*/
function deleteStudent(payload, $btn, originalLabel) {
$.ajax({
url: ajaxUrl,
type: "POST",
dataType: "json",
data: payload,
beforeSend: function () {
$btn.prop("disabled", true).html(
'<i class="fa fa-spinner fa-spin me-1"></i> Processing…',
);
$("#deleteError").addClass("d-none").text("");
},
success: function (res) {
if (res.status) {
$("#studentRow-" + payload.id).fadeOut(300, function () {
$(this).remove();
});
} else {
$("#deleteError").removeClass("d-none").text(res.message);
}
},
error: function (xhr, status, err) {
const msg = xhr.responseJSON?.message ?? `Request failed: ${err}`;
$("#deleteError").removeClass("d-none").text(msg);
},
complete: function () {
$btn.prop("disabled", false).html(originalLabel);
},
});
}
$(document).ready(function () {
$(document).on("click", "#deleteBtn", function () {
const $btn = $(this);
const originalLabel = $btn.html();
const payload = {
action: "delete_student",
id: $btn.data("id"),
};
deleteStudent(payload, $btn, originalLabel);
});
});
PHP output:
<?php
/**
* ! Purpose:
* - Handles AJAX requests for student record operations.
*
* ? Steps:
* - 1. Set JSON response header.
* - 2. Enforce POST-only access.
* - 3. Extract action and payload from the request.
* - 4. Dispatch to the appropriate handler function.
*
* ? Functionality:
* - AJAX handler for the students module.
*
* ! Note:
* - All responses follow the { status, data, message } envelope.
*/
header('Content-Type: application/json');
/**
* ? Sends a JSON response envelope and terminates execution.
*
* @param bool $status Whether the operation succeeded.
* @param mixed $data Payload returned to the client.
* @param string $message Human-readable status message.
* @param int $code HTTP status code.
*
* @return void
*/
function respond(bool $status, mixed $data = [], string $message = '', int $code = 200): void
{
http_response_code($code);
echo json_encode(['status' => $status, 'data' => $data, 'message' => $message]);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
respond(false, [], 'Method not allowed. Use POST.', 405);
}
/**
* ? Deletes a student record by ID.
*
* @param array $payload Request data. Must include `id`.
*
* @return void
*
* ! Note: This example simulates deletion — replace with a real DB query.
*/
function handleDeleteStudent(array $payload): void
{
$id = $payload['id'] ?? null;
if (!$id) {
respond(false, [], 'Student ID is required.', 400);
}
// TODO: replace with real DELETE query
respond(true, ['deleted_id' => $id], 'Student deleted successfully.');
}
/**
* ? Dispatches the incoming action to the correct handler function.
*
* @param string $action Action key from the POST payload.
* @param array $payload Full POST payload.
*
* @return void
*
* ! Note: Responds with 400 if the action key is not registered.
*/
function dispatchAction(string $action, array $payload): void
{
$strategies = [
'delete_student' => 'handleDeleteStudent',
];
$handler = $strategies[$action] ?? null;
if ($handler === null) {
respond(false, [], "Invalid action: {$action}", 400);
}
$handler($payload);
}
$payload = $_POST;
$action = $payload['action'] ?? null;
if (!$action) {
respond(false, [], 'No action provided.', 400);
}
dispatchAction($action, $payload);
What to wire up:
#deleteBtnmust havedata-id="..."on each button in the table row.#studentRow-{id}must be the<tr>id pattern used in the table markup.#deleteErrormust be a<div>or<span>in the template for error display.
Example B — Table load trigger
User says: "Load student records into #studentsTable on page ready."
JS beforeSend:
beforeSend: function () {
const colSpan = $('#studentsTable thead tr th').length;
$('#studentsTable tbody').html(
`<tr><td colspan="${colSpan}" class="text-center py-3">
<i class="fa fa-spinner fa-spin me-1"></i> Loading data…
</td></tr>`
);
},
JS success:
success: function (res) {
if (!res.status) {
const colSpan = $('#studentsTable thead tr th').length;
$('#studentsTable tbody').html(
`<tr><td colspan="${colSpan}" class="text-center text-danger py-3">
${$('<span>').text(res.message).html()}
</td></tr>`
);
return;
}
if (!res.data.length) {
const colSpan = $('#studentsTable thead tr th').length;
$('#studentsTable tbody').html(
`<tr><td colspan="${colSpan}" class="text-center text-muted py-3">
No records found.
</td></tr>`
);
return;
}
let rows = '';
$.each(res.data, function (i, s) {
rows += `<tr id="studentRow-${s.id}">
<td>${i + 1}</td>
<td>${$('<span>').text(s.name).html()}</td>
<td>${$('<span>').text(s.branch).html()}</td>
</tr>`;
});
$('#studentsTable tbody').html(rows);
},
Note the XSS pattern: $('<span>').text(value).html() — set via .text() (auto-escapes),
then read back via .html() to get the escaped string for injection into a template
literal. Use this any time you must embed user-supplied data inside an HTML string.
7. Quick-Reference Checklist
Before delivering output, verify each item:
JS side
-
ajaxUrldeclared as aconstat the file top - AJAX caller is a named function, not an inline callback
-
beforeSendapplies the correct loading state for the trigger type -
colspanin table spinner is derived dynamically, not hardcoded -
$btnandoriginalLabelcaptured at event-bind time, not insidebeforeSend -
successchecksres.statusbefore usingres.data -
errorreadsxhr.responseJSON?.messagefirst, falls back toerr -
completeresets button to original state (for button triggers) - User-supplied data uses
$('<span>').text(val).html()when embedded in HTML strings
PHP side — structure
-
respond()helper present - POST-only enforcement before any logic
- All payload keys validated with
?? null+ earlyrespond(false, ...)on missing - Strategy dispatch array used (not if/elseif chain)
-
dispatchAction()returns 400 on unknown action
PHP side — code quality (php-simplifier)
- php-simplifier SKILL.md was read before writing any PHP
- File-level PHPDoc block present and correctly formatted
- Every function has a PHPDoc block
- PHP 8.1 syntax throughout (match,
??,?->, typed params, short arrays) - No closing
?>tag