javascript-php-integration/SKILL.md
JavaScript integration patterns for PHP+JavaScript SaaS apps. Enforces JS-in-own-files architecture: data passing via data attributes/meta tags, AJAX to PHP API endpoints, CSRF protection, file organization, script loading strategy. Use when...
npx skillsauth add peterbamuhigire/skills-web-dev javascript-php-integrationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
javascript-php-integration or would be better handled by a more specific companion skill.SKILL.md first, then load only the referenced deep-dive files that are necessary for the task.JavaScript belongs in
.jsfiles. PHP emits HTML and data — never JavaScript logic.Allowed in PHP:
<script src="...">,<div data-config='<?= json_encode($data) ?>'>,<meta name="csrf-token" content="<?= $token ?>">. Never in PHP:<script>var x = <?php echo $x; ?></script>(except truly trivial one-liners like page redirects).
assets/
└── js/
├── core/ # Shared infrastructure
│ ├── api.js # Fetch wrapper, error handling
│ ├── auth.js # CSRF token, session management
│ ├── event-bus.js # Global event system
│ └── utils.js # formatCurrency, formatDate, debounce, etc.
├── modules/ # Feature-specific JS (one per page/feature)
│ ├── customers.js # Customer list page
│ ├── invoice-form.js # Invoice creation form
│ ├── dashboard.js # Dashboard charts and stats
│ └── reports.js # Report generation
├── components/ # Reusable UI components
│ ├── confirm-dialog.js
│ ├── data-table.js
│ └── file-uploader.js
└── vendors/ # Third-party libraries (local copies)
├── datatables.min.js
└── chart.min.js
Rule: One module per page/feature. Core files are loaded in the layout. Modules are loaded per page.
PHP passes data to JS via HTML — never via inline script blocks.
<!-- Method 1: data attribute on container element (preferred) -->
<div id="page-data"
data-config='<?= json_encode([
'apiBase' => '/api',
'currencySymbol' => $settings->currency_symbol,
'dateFormat' => $settings->date_format,
'tenantId' => $tenant->id,
], JSON_HEX_APOS | JSON_HEX_TAG) ?>'>
</div>
<!-- Method 2: CSRF token in meta tag (for all AJAX calls) -->
<meta name="csrf-token" content="<?= htmlspecialchars($csrfToken) ?>">
<!-- Method 3: Per-page data on the page's own container -->
<div id="customers-table"
data-filters='<?= json_encode($activeFilters, JSON_HEX_APOS) ?>'
data-permissions='<?= json_encode($userPermissions, JSON_HEX_APOS) ?>'>
</div>
// assets/js/core/config.js — Read all page config once at startup
const AppConfig = (() => {
const el = document.getElementById('page-data');
if (!el) return {};
try {
return JSON.parse(el.dataset.config);
} catch {
console.error('Invalid page config JSON');
return {};
}
})();
export const { apiBase, currencySymbol, dateFormat, tenantId } = AppConfig;
Why JSON_HEX_APOS | JSON_HEX_TAG: Prevents HTML injection when the JSON is placed inside an HTML attribute. Always use these flags.
// assets/js/core/auth.js
export function getCsrfToken() {
return document.querySelector('meta[name="csrf-token"]')?.content ?? '';
}
// assets/js/core/api.js
import { getCsrfToken } from './auth.js';
export async function apiPost(endpoint, data) {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken(),
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify(data)
});
if (!response.ok) throw await response.json();
return response.json();
}
export async function apiGet(endpoint, params = {}) {
const url = new URL(endpoint, window.location.origin);
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
const response = await fetch(url, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
if (!response.ok) throw await response.json();
return response.json();
}
X-Requested-With: XMLHttpRequest — PHP checks this to block direct browser URL access to API endpoints. Always include it on every AJAX call.
<?php
// api/customers.php
header('Content-Type: application/json');
// Verify AJAX — block direct URL access
if (empty($_SERVER['HTTP_X_REQUESTED_WITH'])) {
http_response_code(403);
exit(json_encode(['error' => 'Direct access forbidden']));
}
// Verify CSRF on state-changing requests
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$token = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if (!hash_equals($_SESSION['csrf_token'], $token)) {
http_response_code(403);
exit(json_encode(['error' => 'Invalid CSRF token']));
}
}
// Route the action
$action = $_GET['action'] ?? '';
switch ($action) {
case 'list':
echo json_encode(['data' => $customerService->getAll()]);
break;
case 'save':
$body = json_decode(file_get_contents('php://input'), true);
$result = $customerService->save($body);
echo json_encode(['data' => $result]);
break;
default:
http_response_code(400);
echo json_encode(['error' => 'Unknown action']);
}
Checklist for every PHP endpoint:
Content-Type: application/json header firstHTTP_X_REQUESTED_WITH before anything elsehash_equals() for CSRF comparison (timing-safe){ data: ... } or { error: ... } shape<!-- layout.php: load core scripts once in every page -->
<script src="/assets/js/vendors/jquery.min.js"></script>
<script src="/assets/js/core/config.js" type="module"></script>
<script src="/assets/js/core/api.js" type="module"></script>
<script src="/assets/js/core/event-bus.js" type="module"></script>
<!-- Individual PHP pages declare which module they need -->
<?php $pageScript = 'modules/customers.js'; ?>
<!-- Layout footer picks it up -->
<?php if (!empty($pageScript)): ?>
<script src="/assets/js/<?= htmlspecialchars($pageScript) ?>" type="module"></script>
<?php endif; ?>
Rules:
type="module" on all app JS — enables ES module imports and defers by default$pageScript — the layout includes it// assets/js/modules/customers.js
import { apiGet } from '../core/api.js';
import { EventBus } from '../core/event-bus.js';
import { getCsrfToken } from '../core/auth.js';
const el = document.getElementById('customers-table');
const permissions = JSON.parse(el?.dataset.permissions ?? '{}');
const table = new DataTable('#customers-table', {
ajax: {
url: '/api/customers?action=list',
type: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-Token': getCsrfToken()
},
dataSrc: 'data'
},
columns: [
{ data: 'id' },
{ data: 'name' },
{ data: 'email' },
{
data: 'id',
render: (id) => permissions.canEdit
? `<button data-action="edit" data-id="${id}">Edit</button>`
: ''
}
]
});
// Event delegation — works for dynamically-rendered rows
el.addEventListener('click', e => {
const btn = e.target.closest('[data-action]');
if (!btn) return;
if (btn.dataset.action === 'edit') {
EventBus.emit('customer:edit', { id: btn.dataset.id });
}
if (btn.dataset.action === 'delete') {
EventBus.emit('customer:delete', { id: btn.dataset.id });
}
});
// assets/js/modules/customer-form.js
import { apiPost } from '../core/api.js';
import { EventBus } from '../core/event-bus.js';
document.getElementById('customer-form')?.addEventListener('submit', async e => {
e.preventDefault();
const form = e.target;
const submitBtn = form.querySelector('[type=submit]');
submitBtn.disabled = true;
submitBtn.textContent = 'Saving...';
clearErrors(form);
try {
const data = Object.fromEntries(new FormData(form));
const result = await apiPost('/api/customers?action=save', data);
EventBus.emit('customer:saved', result.data);
Swal.fire('Saved!', 'Customer saved successfully.', 'success');
} catch (error) {
if (error.errors) showFieldErrors(form, error.errors);
else Swal.fire('Error', error.message ?? 'Save failed', 'error');
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'Save';
}
});
function showFieldErrors(form, errors) {
Object.entries(errors).forEach(([field, message]) => {
const input = form.querySelector(`[name="${field}"]`);
if (!input) return;
input.classList.add('is-invalid');
const feedback = input.nextElementSibling;
if (feedback?.classList.contains('invalid-feedback')) {
feedback.textContent = message;
}
});
}
function clearErrors(form) {
form.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid'));
}
| Allowed in PHP | Prohibited in PHP |
|---|---|
| <script src="/assets/js/page.js" type="module"> | <script>var config = <?php echo json_encode($data); ?></script> |
| <meta name="csrf-token" content="<?= $token ?>"> | <script>if (<?= $role ?> === 'admin') { ... }</script> |
| <div data-config='<?= json_encode($cfg, JSON_HEX_APOS) ?>'> | onclick="deleteRecord(<?= $id ?>)" |
| <div id="chart" data-stats='<?= json_encode($stats) ?>'> | <script>$.ajax({ url: '<?= $url ?>' })</script> |
Exception: Simple redirects after server actions are acceptable:
<script>window.location.href = '<?= htmlspecialchars($redirectUrl) ?>';</script>
Decouples modules that need to communicate without direct imports.
// assets/js/core/event-bus.js
const listeners = {};
export const EventBus = {
on(event, callback) {
(listeners[event] ??= []).push(callback);
},
off(event, callback) {
listeners[event] = (listeners[event] ?? []).filter(cb => cb !== callback);
},
emit(event, payload) {
(listeners[event] ?? []).forEach(cb => cb(payload));
}
};
Usage pattern:
// customers.js emits
EventBus.emit('customer:saved', { id: 42, name: 'Acme Ltd' });
// dashboard.js listens — no direct import of customers.js needed
EventBus.on('customer:saved', ({ id, name }) => {
refreshCustomerCount();
});
X-Requested-With and X-CSRF-Token are presenterror_log(json_encode($data)) to confirm what the endpoint receivesconsole.log(JSON.parse(document.getElementById('page-data').dataset.config)) to verify data attribute passing$_SESSION['csrf_token'] is set before the page renders; regenerate on login?action=list)HTTP_X_REQUESTED_WITH is missing or CSRF token mismatch<?php ?> inside <script> tags (except redirects)data-* attributes with json_encode() and JSON_HEX_APOS | JSON_HEX_TAG<meta name="csrf-token">, read only by core/auth.jsX-Requested-With: XMLHttpRequestX-CSRF-Token headerHTTP_X_REQUESTED_WITH before respondingcore/, modules/, components/, vendors/type="module" on all app script tagsonclick="..." attributes with PHP-echoed valuesdata-ai
Use when adding AI-powered analytics to a SaaS platform — semantic search over business data, natural language queries, trend detection, anomaly alerts, and AI-generated insights for dashboards. Covers embeddings, NL2SQL, and per-tenant analytics...
data-ai
Design AI-powered analytics dashboards — what metrics to show, how to display AI predictions and confidence, drill-down patterns, KPI cards, trend visualisation, AI Insights panels, export design, and role-based dashboard variants. Invoke when...
development
Use when designing, building, reviewing, or upgrading production software systems that must be secure, performant, maintainable, scalable, and user-centered. Apply before writing specs, code, architecture, APIs, databases, mobile apps, SaaS platforms, or ERP systems.
development
Professional web app UI using commercial templates (Tabler/Bootstrap 5) with strong frontend design direction when needed. Use for CRUD interfaces, dashboards, admin panels with SweetAlert2, DataTables, Flatpickr. Clone seeder-page.php, use...