In PHP, how do you make simplexml_load_string() or DOMDocument::loadXML() report malformed XML as data instead of emitting warnings?
answer
- switch libxml to collect errors
- the loader returns false
- LibXMLError: level, code, line, column
- clear the buffer afterwards
- restore the previous setting
basics
~10 sCall libxml_use_internal_errors(true) before parsing; the loader then returns false silently and libxml_get_errors() returns LibXMLError objects with message, line and column. Call libxml_clear_errors() and restore the previous setting afterwards.
solid answer
~40 sBy default, libxml parse problems surface as PHP `E_WARNING`s from `simplexml_load_string()`, `DOMDocument::loadXML()` and friends, which is noisy and hard to act on. `libxml_use_internal_errors(true)` switches libxml to **collect** errors instead; it returns the previous setting so you can restore it. The loader still returns `false` on a fatal error, and `libxml_get_errors()` returns an array of `LibXMLError` objects with `level` (`LIBXML_ERR_WARNING`, `LIBXML_ERR_ERROR`, `LIBXML_ERR_FATAL`), `code`, `message`, `file`, `line` and `column`, ready to log or return to a supplier. The buffer is not emptied by reading it, so call `libxml_clear_errors()`, especially in a long-running worker. Silencing with `@` or `LIBXML_NOERROR` hides the details instead of capturing them.
code
php · 25 lines<?php
declare(strict_types=1);
/** @return list<string> */
function parseFeed(string $xml, ?SimpleXMLElement &$feed): array
{
$previous = libxml_use_internal_errors(true);
libxml_clear_errors();
try {
$result = simplexml_load_string($xml);
$messages = array_map(
fn(LibXMLError $e): string => sprintf('line %d, col %d: %s', $e->line, $e->column, trim($e->message)),
libxml_get_errors(),
);
$feed = $result === false ? null : $result;
return $messages;
} finally {
libxml_clear_errors();
libxml_use_internal_errors($previous);
}
}
$errors = parseFeed('<feed><product sku="A-100"></feed>', $feed);
var_dump($feed); // NULL
print_r($errors); // e.g. line 1, col ...: Opening and ending tag mismatch ...go deeper
Recall that malformed XML makes the loader return false and that libxml_use_internal_errors(true) plus libxml_get_errors() gives you the messages.
Explain the collect, read, clear, restore sequence and what LibXMLError carries, including levels that do not cause failure.
Build one wrapper that reports supplier-facing positions, never leaks global state, and keeps long-running workers from accumulating errors.
Decide how malformed partner input is reported back and measured, so rejected feeds become actionable tickets rather than log noise.
## The default: warnings Every libxml-based loader in PHP reports parse problems through PHP's error system. For a truncated feed, `simplexml_load_string()` returns `false` and emits one `E_WARNING` per libxml message, typically a "Premature end of data" parser error. That is awkward for an import job: - warnings go to the log with no link to the input that caused them; - a global error handler may turn them into exceptions at unexpected places; - you cannot easily show the supplier "line 812, column 14: ...". ## Collecting errors instead `libxml_use_internal_errors(?bool $use_errors = null): bool` changes where errors go: - `true`: libxml stores errors in an internal buffer; no warnings are emitted; - `false`: back to warnings; - `null`: leave the setting unchanged and only report it. It **returns the previous value**, which makes a clean save-and-restore possible. With collection on, the pattern is: 1. `$previous = libxml_use_internal_errors(true);` 2. parse: `$feed = simplexml_load_string($xml);` or `$ok = $dom->loadXML($xml);` 3. if the result is `false`, read `libxml_get_errors()`; 4. `libxml_clear_errors();` 5. `libxml_use_internal_errors($previous);` ## What an error looks like `libxml_get_errors()` returns a list of `LibXMLError` objects with typed public properties: | Property | Meaning | |---|---| | `level` | `LIBXML_ERR_WARNING`, `LIBXML_ERR_ERROR` or `LIBXML_ERR_FATAL` | | `code` | libxml's numeric error code | | `message` | the human-readable text (ends with a newline) | | `file` | the file name when loading from a file, otherwise empty | | `line`, `column` | position in the input | `libxml_get_last_error()` returns only the most recent one, or `false` when there is none. Only fatal errors make the loader return `false`. Warnings and recoverable errors can be present even when parsing succeeds, so check the buffer when you care about them too. ## Clearing matters in long-running processes The buffer is **not** emptied when you read it. In a queue worker or a long-lived process that parses thousands of feeds: - errors from one document are reported again with the next one; - the buffer keeps growing in memory. Always call `libxml_clear_errors()` after handling them, and before parsing if you are not sure what ran earlier. ## Alternatives that lose information | Technique | Effect | |---|---| | `@simplexml_load_string($xml)` | hides the warnings; details are gone | | `LIBXML_NOERROR \| LIBXML_NOWARNING` options | suppresses reports inside libxml; details are gone | | `new SimpleXMLElement($xml)` | throws an `Exception` ("String could not be parsed as XML"); warnings are still emitted unless internal errors are on | For an import you need to explain, internal errors are the only one of these that keeps line, column and message. ## Wrapping it once Put the save/parse/collect/clear/restore sequence in one function that returns either the parsed document or a list of formatted messages. Every caller then gets the same behaviour, and no code path forgets to restore the global setting. ## What changes per loader | Loader | Failure result | Where messages go by default | |---|---|---| | `simplexml_load_string()` / `simplexml_load_file()` | `false` | `E_WARNING` per message | | `DOMDocument::loadXML()` / `load()` | `false` | `E_WARNING` per message | | `new SimpleXMLElement($xml)` | throws `Exception` ("String could not be parsed as XML") | `E_WARNING` per message, plus the exception | | `XMLReader::read()` | `false`, ending the loop | `E_WARNING` per message | In every row, `libxml_use_internal_errors(true)` moves the messages into the buffer; the failure result itself is unchanged.
- Why must a queue worker call libxml_clear_errors() after every document?The error buffer lives for the whole process and reading it does not empty it. Without clearing, the next document's report includes the previous document's errors, and the buffer keeps growing in memory across thousands of jobs.
- Can parsing succeed and still leave entries in libxml_get_errors()?Yes. Only fatal errors make the loader return `false`. Warnings and recoverable errors are collected too, so a document can load while the buffer holds entries at `LIBXML_ERR_WARNING` or `LIBXML_ERR_ERROR` level. Filter by `level` if you only act on failures.
saying these in an interview costs you the question
- Using @ on the loader and losing the line and column
- Assuming libxml_get_errors() empties the buffer
- Forgetting to restore the previous libxml_use_internal_errors() value
- Expecting simplexml_load_string() to throw on malformed XML
- Believing internal errors change what the loader returns