AJAX XML
Exchanging XML With AJAX In simple terms: The "X" in AJAX originally stood for XML: early AJAX applications exchanged XML documents between browser and server. Today JSON is the usual choice, but XML
Exchanging XML With AJAX
In simple terms: The "X" in AJAX originally stood for XML: early AJAX applications exchanged XML documents between browser and server. Today JSON is the usual choice, but XML still appears — when integrating with older systems, consuming news feeds, or working with industry formats. This lesson shows both directions: PHP producing XML for JavaScript, and PHP receiving XML sent by JavaScript.
Technically: A PHP endpoint returns XML with the Content-Type: application/xml header, generated safely with XMLWriter, DOM, or SimpleXML. In the browser, fetch() reads the response as text, and the built-in DOMParser turns it into an XML document that can be queried with querySelector() or getElementsByTagName(). For XML sent to PHP, the endpoint reads the raw body from php://input and parses it with SimpleXML.
XML or JSON for AJAX?
| JSON | XML | |
|---|---|---|
| Parsing in JavaScript | response.json() — native objects | DOMParser — DOM queries |
| Size and readability | Compact | More verbose |
| Attributes, namespaces, schemas | No | Yes |
| Typical use today | New APIs, most AJAX | Feeds (RSS/Atom), legacy services, standardized formats |
Use JSON unless an existing system or format requires XML.
PHP Producing XML: api/courses.xml.php
<?php
declare(strict_types=1);
$courses = [
['id' => 'php', 'title' => 'PHP Full Course', 'weeks' => 12, 'level' => 'Beginner'],
['id' => 'mysql', 'title' => 'MySQL Essentials', 'weeks' => 6, 'level' => 'Beginner'],
['id' => 'laravel', 'title' => 'Laravel Framework', 'weeks' => 10, 'level' => 'Intermediate'],
];
$level = $_GET['level'] ?? '';
if ($level !== '') {
$courses = array_filter($courses, fn(array $course): bool => $course['level'] === $level);
}
header('Content-Type: application/xml; charset=utf-8');
$writer = new XMLWriter();
$writer->openUri('php://output');
$writer->setIndent(true);
$writer->setIndentString(' ');
$writer->startDocument('1.0', 'UTF-8');
$writer->startElement('courses');
foreach ($courses as $course) {
$writer->startElement('course');
$writer->writeAttribute('id', $course['id']);
$writer->writeAttribute('level', $course['level']);
$writer->writeElement('title', $course['title']);
$writer->writeElement('weeks', (string) $course['weeks']);
$writer->endElement();
}
$writer->endElement();
$writer->endDocument();
$writer->flush();
Response for api/courses.xml.php
<?xml version="1.0" encoding="UTF-8"?>
<courses>
<course id="php" level="Beginner">
<title>PHP Full Course</title>
<weeks>12</weeks>
</course>
<course id="mysql" level="Beginner">
<title>MySQL Essentials</title>
<weeks>6</weeks>
</course>
<course id="laravel" level="Intermediate">
<title>Laravel Framework</title>
<weeks>10</weeks>
</course>
</courses>
With ?level=Intermediate, only the Laravel course is included. In a real application, the courses come from the database. XMLWriter escapes all values automatically (see PHP XMLWriter); the level parameter is only compared, never written into the output unchecked.
JavaScript Reading XML
<select id="level">
<option value="">All levels</option>
<option value="Beginner">Beginner</option>
<option value="Intermediate">Intermediate</option>
</select>
<ul id="course-list"></ul>
<script>
const levelSelect = document.getElementById('level');
const list = document.getElementById('course-list');
async function loadCourses() {
const params = new URLSearchParams({ level: levelSelect.value });
const response = await fetch(`api/courses.xml.php?${params}`);
if (!response.ok) {
list.replaceChildren(new Text('Could not load courses.'));
return;
}
const xml = new DOMParser().parseFromString(await response.text(), 'application/xml');
if (xml.querySelector('parsererror')) {
list.replaceChildren(new Text('The server sent invalid XML.'));
return;
}
const items = [...xml.querySelectorAll('course')].map(course => {
const li = document.createElement('li');
const title = course.querySelector('title').textContent;
const weeks = course.querySelector('weeks').textContent;
li.textContent = `${title} — ${weeks} weeks (${course.getAttribute('level')})`;
return li;
});
list.replaceChildren(...items);
}
levelSelect.addEventListener('change', loadCourses);
loadCourses();
</script>
Result in the Browser (All Levels)
• PHP Full Course — 12 weeks (Beginner)
• MySQL Essentials — 6 weeks (Beginner)
• Laravel Framework — 10 weeks (Intermediate)
How It Works
response.text()reads the body as a string;DOMParserwith the typeapplication/xmlturns it into an XML document.- Malformed XML does not throw: the parser returns a document containing a
<parsererror>element, which must be checked explicitly. querySelectorAll('course'),querySelector('title'),textContent, andgetAttribute()work on XML documents just as on HTML.- List items are created with
textContent, so course titles can never inject HTML.
JavaScript Sending XML to PHP
Some systems expect XML requests — for example, an order sent to a warehouse service. PHP reads the raw body and parses it:
<?php
// api/stock-check.php — receives <request><sku>…</sku>…</request>
declare(strict_types=1);
libxml_use_internal_errors(true);
$body = file_get_contents('php://input', length: 100_000); // limit the size
$request = $body === false ? false : simplexml_load_string($body);
header('Content-Type: application/xml; charset=utf-8');
$response = new SimpleXMLElement('<response/>');
if ($request === false || $request->getName() !== 'request') {
http_response_code(400);
$response->error = 'Invalid XML request.';
echo $response->asXML();
exit;
}
$stock = ['PEN-01' => 118, 'LAMP-01' => 6, 'BAG-02' => 0]; // normally from the database
foreach ($request->sku as $sku) {
$code = strtoupper(trim((string) $sku));
$item = $response->addChild('item');
$item->addAttribute('sku', $code);
$item->addAttribute('available', isset($stock[$code]) && $stock[$code] > 0 ? 'yes' : 'no');
}
echo $response->asXML();
Request and Response
const response = await fetch('api/stock-check.php', {
method: 'POST',
headers: { 'Content-Type': 'application/xml' },
body: '<request><sku>LAMP-01</sku><sku>BAG-02</sku></request>',
});
console.log(await response.text());
<?xml version="1.0"?>
<response><item sku="LAMP-01" available="yes"/><item sku="BAG-02" available="no"/></response>
Key Points
- The body is read from
php://inputwith a maximum length, protecting against oversized requests. - Parsing uses libxml's secure defaults; never add
LIBXML_NOENTorLIBXML_DTDLOADfor request bodies (see PHP XML Parsers). - Every value is cast and normalized before use; the response is built with SimpleXML, so attribute values are escaped.
- Invalid input produces a 400 response in the same XML format the client expects.
Serving External Feeds: A Server-Side Proxy
Browsers usually cannot fetch RSS feeds from other domains directly, because of the same-origin policy (see AJAX Intro). A PHP endpoint can fetch the feed on the server, extract the needed data, and return it as JSON:
<?php
// api/blog-feed.php (simplified)
$xml = simplexml_load_string(httpGet('https://example.com/feed.xml')); // fetch with cURL and timeouts
$items = [];
foreach ($xml->channel->item as $item) {
$items[] = ['title' => (string) $item->title, 'link' => (string) $item->link];
if (count($items) === 5) {
break;
}
}
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: public, max-age=600');
echo json_encode(['items' => $items], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
Only fetch fixed, trusted feed URLs — letting users pass arbitrary URLs to such a proxy creates a server-side request forgery (SSRF) risk. Cache the result, so the external site is not contacted on every page view.
Common Mistakes
Missing or Wrong Content-Type
Effect: Clients misinterpret the response.
Fix: Send application/xml; charset=utf-8 for XML responses.
Not Checking for <parsererror>
Effect: JavaScript processes an error document as if it were data.
Fix: Check xml.querySelector('parsererror') after parsing.
Building XML Responses With String Concatenation
Effect: Broken XML when values contain & or <.
Fix: Use XMLWriter, DOM, or SimpleXML.
Unlimited Request Bodies
Effect: Memory exhaustion from huge uploads.
Fix: Read php://input with a length limit and enforce server limits.
Best Practices
- Prefer JSON for new AJAX features; use XML when a format or system requires it.
- Generate XML with an XML API and send the correct content type.
- Parse XML in the browser with
DOMParserand check for parse errors. - Parse incoming XML in PHP with size limits, libxml's secure defaults, and full validation.
- Use server-side proxies with fixed URLs and caching to integrate external feeds.
Official reference: https://www.php.net/manual/en/book.simplexml.php