XML Parsing with Expat

Use PHP's event-based XML parser with element and character handlers, chunked input, final parsing, and precise error reporting.

Lesson 62 of 86Intermediate10 min read

A tree parser stores the structure of the whole XML document. An event parser instead calls your functions as it encounters opening elements, text, and closing elements. PHP exposes this style through its XML parser extension, often described as Expat-style parsing. That makes it useful for feeds whose records can be processed in order. The price is that your code must keep track of the current record and assemble text correctly. This reference explains handlers, parser options, chunked input, finalization, and error locations. It uses modern PHP's parser object rather than treating it as an old resource handle.

Follow the stream of events

The parser announces a start element with its name and attributes, may announce character data one or several times, and announces an end element. Your handlers receive the parser plus event details. A small state machine keeps the current path, current product, and current field. When a product begins, initialize a record. When its quantity begins, reset a text buffer. Append every character-data fragment while that field is active. When quantity ends, validate the complete buffer. When product ends, emit or store the complete record. This design is clearer than trying to infer structure from the most recent callback alone. Nested elements and whitespace can produce events you do not care about, so explicitly define which paths the feed supports.

Character data can arrive in fragments

Never assume one callback contains a complete text value. Chunk boundaries, entities, encoding conversion, and parser internals can split a logical value across several events. Replacing the buffer on each callback loses earlier text; appending preserves it. Trim after the closing element if the field's rules allow surrounding whitespace, rather than trimming every fragment separately and accidentally removing meaningful spaces. A note containing 'blue ' and 'pen' in separate callbacks should remain 'blue pen'. Limit the accumulated field length so a single huge value cannot exhaust memory. Validate once a complete field is available. Event parsing avoids a full document tree, but your handler can still recreate an unbounded memory problem if it collects every record into one giant array.

Configure names and namespaces intentionally

The XML parser supports case-folding options. Legacy examples may silently uppercase names, which can surprise code written for lowercase feed elements. Set XML_OPTION_CASE_FOLDING explicitly so the handler receives the names your contract expects. Namespaces need their own decision: xml_parser_create_ns() can expose namespace-qualified names separated by a configured separator. Compare expanded names according to the namespace URI rather than relying only on a source prefix. Attribute names and values arrive separately from character data. Do not assume an attribute exists because the XML parses correctly. Required attributes, allowed element ordering, and unexpected nesting are application checks. A parser's grammar knowledge does not automatically enforce your product-feed structure.

Parse chunks and signal the true end

Create the parser, register handlers, and call xml_parse() for each chunk. The final argument tells the parser when no more input will arrive. A document missing its closing tag may not be recognized as incomplete until finalization, so a successful intermediate chunk is not proof of a complete valid document. For file streams, read bounded chunks and handle fread() failure distinctly from end of file. Feed the final empty chunk with the final flag if that keeps the loop logic clear. Avoid committing database records merely because their closing tags appeared if your acceptance policy requires the entire document to be valid. Stage records or wrap the import in a transaction under a size-appropriate policy.

Report syntax errors with useful context

xml_parse() indicates success or failure. On failure, xml_get_error_code() and xml_error_string() identify the parser problem, while line and column functions locate it. Include a record or job identifier in the private log. User-facing responses should explain that the document is invalid without dumping confidential XML or infrastructure paths. A callback can also fail because of business rules, such as a negative stock quantity; distinguish that from malformed XML syntax. Use a consistent cleanup boundary around the stream and parser references. In PHP 8, xml_parser_create() returns an XMLParser object, and normal object lifetime manages its memory. Old examples centered on resource types and explicit free calls need version-aware review rather than copying them unchanged.

Use event parsing within a safe import design

Bound input bytes, nesting depth, record count, and accumulated text. Reject unsupported DTD or external-entity features under the feed contract and do not install a handler that fetches arbitrary external entities. Validate any URL separately before downloading XML. For a supplier import, stage accepted records and mark the job complete only after final parse success. Decide what to do when a later syntax error follows earlier valid records. A transaction, staging table, or documented partial acceptance policy makes the outcome predictable. A tree parser may be simpler for small documents that need arbitrary navigation. Choose streaming because it supports the data volume and record flow, not merely because the word streaming sounds faster.

Practice task and worked solution

Extend the state machine to read product code attributes and quantity elements. Reset the quantity buffer on its start event, append text fragments, and validate a nonnegative integer on its end event. Emit the record only at the product end. The solution must produce the same records when input arrives as one large chunk or many small chunks. Test whitespace around quantity, two consecutive products, a missing code, and a document truncated near the end. Add a maximum field length and nesting depth. Do not collect every emitted record in memory for a large import; stage or process bounded batches under the documented failure policy.

Continue learning

Use functions for callbacks, file handling for chunked reads, and exceptions for import failure boundaries.

The XML parser reference documents the PHP 8 XMLParser return type.

XML Parsing with Expat Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Count elements through callbacks

This self-contained CLI example sends a fixed document in two chunks. The parser has no external-entity handler and does not build a tree.

Expected output is 2 products. If the final closing products tag is removed, final parsing fails. Availability depends on the XML parser extension installed with PHP.

PHP
<?php
$count = 0;
$parser = xml_parser_create('UTF-8');
xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false);
xml_set_element_handler(
    $parser,
    function (XMLParser $parser, string $name, array $attributes) use (&$count): void {
        if ($name === 'product') $count++;
    },
    function (XMLParser $parser, string $name): void {}
);
$chunks = ['<products><product code="a"/>', '<product code="b"/></products>'];
foreach ($chunks as $index => $chunk) {
    if (!xml_parse($parser, $chunk, $index === count($chunks) - 1)) {
        throw new RuntimeException(xml_error_string(xml_get_error_code($parser)));
    }
}
echo $count . ' products' . PHP_EOL;
Output
2 products

XML Parsing with Expat FAQ

Will one text callback contain a whole field?

No. Expat may call your character-data handler several times for one element, splitting text at buffer boundaries or around entities. Append fragments to a buffer in the handler, and process the complete text in the end-element handler.

Does an intermediate successful parse prove validity?

No. When you feed a document in chunks, each xml_parse() call only reports problems found so far. Finalization, the last call with the final-chunk flag set to true, detects incomplete documents such as a missing closing tag, so always check that last result too.

Why are names uppercase?

Expat's case folding may be enabled, which converts element and attribute names to uppercase before they reach your handlers. Set the option explicitly with xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false) so names match the document exactly.

Do I need a tree?

Only if later navigation or editing requires one. An event-based parser processes each element as it arrives and uses very little memory, which suits large feeds you read once. If you need to move around the document, query it with XPath or change it, DOM or SimpleXML is easier.

XML Parsing with Expat Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choicemedium

A large parser appends every record to one array. What improves bounded memory?

XML Parsing with Expat Quiz

2 questions to check what you learned.

  1. How should character-data handlers assemble a field?

    • Append every fragment until the field closes
    • Replace the value each time
    • Assume one callback always suffices
    • Convert every fragment to integer
  2. Why must the final parse call indicate completion?

    • It encrypts the feed
    • The parser must detect incomplete document structure
    • It disables all validation
    • It creates a DOM tree