veewee/xml
veewee/xml provides a type-safe, declarative API for working with XML in PHP, covering DOM, encoding, error handling, memory-safe readers and writers, XSD schemas, and XSLT. It handles errors for you and supports spec compliance on PHP 8.4+.
Installation: Add the package via Composer:
composer require veewee/xml
First Use Case: Generate a secure XML file with DOCTYPE disallowed:
use VeeWee\Xml\Writer\Writer;
use function VeeWee\Xml\Writer\Builder\document;
use function VeeWee\Xml\Writer\Builder\element;
use function VeeWee\Xml\Writer\Builder\value;
use function VeeWee\Xml\Writer\Configurator\disallow_doctype;
$writer = Writer::forFile('output.xml');
$writer->configure(disallow_doctype());
$writer->write(
document('1.0', 'UTF-8',
element('root', value('Secure XML without DOCTYPE!'))
)
);
This creates output.xml with:
<?xml version="1.0" encoding="UTF-8"?>
<root>Secure XML without DOCTYPE!</root>
Key Files to Explore:
docs/writer.md: For XML generation (security configurators live here).docs/reader.md: For parsing XML.docs/encoding.md: For JSON-like XML encoding/decoding.docs/security.md: For the DOCTYPE policy and libxml-related safety guidance.v4.14.0 upgrade note: In-memory writes are now faster (Writer::inMemory()), so the common "build an XML string in PHP" pattern — for HTTP responses, tests, and queue payloads — is cheaper. libxml issue detection is also new; see Gotchas.
use VeeWee\Xml\Writer\Writer;
use function VeeWee\Xml\Writer\Builder\{document, element, value};
use function VeeWee\Xml\Writer\Configurator\disallow_doctype;
$writer = Writer::forFile('secure.xml');
$writer->configure(disallow_doctype()); // Enforce no-DOCTYPE policy
$writer->write(
document('1.0', 'UTF-8',
element('data',
element('item', value('Sensitive Data'))
)
)
);
The in-memory writer is the right choice when you need the XML as a string rather than a file handle:
use VeeWee\Xml\Writer\Writer;
use function VeeWee\Xml\Writer\Configurator\disallow_doctype;
$writer = Writer::inMemory()->configure(disallow_doctype());
$writer->write($structure);
$xml = $writer->map(memory_output()); // string, no DOCTYPE
Prefer this over Writer::forFile() + re-read when the consumer is an HTTP response, an event payload, or a test assertion.
Use the Reader for large XML files. Note that reader-side security (entity loading, network access) is a reader concern — disallow_doctype() is a writer configurator and does not protect you when parsing untrusted input:
use VeeWee\Xml\Reader\Reader;
$reader = Reader::fromFile('large_file.xml');
foreach ($reader as $node) {
// Process node-by-node without loading the whole document
}
For untrusted input, configure the reader to disable external entities and network access, then decode/validate the resulting data.
use function VeeWee\Xml\encoding\{xml_encode, xml_decode};
$data = ['name' => 'John', 'age' => 30];
$xml = xml_encode($data); // Converts to XML string
$decoded = xml_decode($xml); // Converts back to array
When the source is untrusted, decode first and validate the resulting data structure rather than trusting the raw string.
use function VeeWee\Xml\Writer\Builder\{element, namespace_attribute, prefixed_element};
use function VeeWee\Xml\Writer\Configurator\disallow_doctype;
$writer = Writer::forFile('namespaced.xml');
$writer->configure(disallow_doctype());
$writer->write(
element('root',
namespace_attribute('http://example.com', 'ex'),
prefixed_element('ex', 'item', value('Test'))
)
);
use VeeWee\Xml\XSD\Validator;
$validator = new Validator('schema.xsd');
$isValid = $validator->validate('data.xml');
use VeeWee\Xml\XSLT\Transformer;
$transformer = new Transformer('style.xsl');
$result = $transformer->transform('input.xml');
Register the package with security defaults in AppServiceProvider:
public function boot()
{
$this->app->singleton(Writer::class, fn () =>
Writer::forFile(storage_path('app/xml/output.xml'))
->configure(disallow_doctype())
);
}
Validate the parsed data, not the raw string, and enforce reader-side safety on input:
use function VeeWee\Xml\encoding\xml_decode;
public function validateXml(): array
{
$data = xml_decode($this->input('xml')); // parse untrusted input safely
return validator($data, [
'name' => 'required|string',
// ...
])->validate();
}
Remember: disallow_doctype() is a writer configurator. Incoming XML needs reader-side entity/network controls, not the writer.
Build the payload in-memory (now faster in v4.14.0) rather than round-tripping through a file:
use VeeWee\Xml\Writer\Writer;
use function VeeWee\Xml\Writer\Configurator\disallow_doctype;
$writer = Writer::inMemory()->configure(disallow_doctype());
$writer->write($structure);
$xml = $writer->map(memory_output());
return response($xml, 200, ['Content-Type' => 'application/xml']);
use VeeWee\Xml\Writer\Writer;
use function VeeWee\Xml\Writer\Configurator\disallow_doctype;
class GenerateXmlJob implements ShouldQueue
{
public function handle()
{
$writer = Writer::forFile(storage_path('app/xml/report.xml'))
->configure(disallow_doctype());
$writer->write($this->buildXmlStructure());
}
}
For jobs that only need the string (e.g. to POST upstream), use Writer::inMemory() and take advantage of the v4.14.0 speedup.
PHP 8.4+ Requirement for v4+:
veewee/xml@^4.0, ensure your project runs on PHP 8.4+.^3.3.Writer-side vs Reader-side security:
disallow_doctype() configures the writer. It prevents you from emitting a DOCTYPE — it does not protect you when parsing untrusted XML.Memory-Safe Reader vs. DOM:
Reader is memory-efficient for large files, but lacks DOM traversal methods.DOM component for complex XPath queries or DOM manipulation.Namespaces:
namespace_attribute will cause malformed XML.Raw XML Injection:
raw() builder bypasses escaping. Only use it for trusted content.Error Handling:
try {
$validator->validate('data.xml');
} catch (\Throwable $e) {
Log::error($e->getMessage());
}
Indentation Quirks:
Writer, not Reader or DOM.DOCTYPE Security (introduced v4.12.0):
disallow_doctype() for writer output in production:
$writer->configure(disallow_doctype());
libxml Issue Detection (new in v4.14.0):
libxml-flagged problems earlier — useful when auditing legacy XML that may have loaded external entities or contains malformed constructs.Inspect In-Memory Output:
$writer = Writer::inMemory()->configure(disallow_doctype());
$writer->write($xmlStructure);
$output = $writer->map(memory_output());
dd($output); // inspect raw XML (should lack DOCTYPE); v4.14.0 makes this cheaper
Validate Against a Schema:
$validator = new Validator('schema.xsd');
$errors = $validator->validate('data.xml', true); // return errors instead of throwing
dd($errors);
Audit Generated vs Incoming XML Separately:
Performance with Large Files:
Reader over DOM for files >10MB.Writer::inMemory() for cases where you actually need the string (it is faster than ever, but streaming still wins on peak memory).Custom Builders:
Implement your own builder functions that return Builder closures and compose them with the built-ins. Custom builders must honour security configurators — especially DOCTYPE and entity handling — so they do not silently bypass the policy.
Custom Configurators:
Wrap libxml options (e.g. LIBXML_NONET, LIBXML_NOENT) for both Writer and Reader via configurators. This is the recommended way to centralise hardening across an application.
Map / Post-Processing Hooks:
Use map() to post-process output, add headers, or route generated XML to storage. Combine with Writer::inMemory() for in-request transformations.
Laravel Macros & Container Bindings:
Bind Writer / Reader singletons with production defaults (no DOCTYPE, no network) in a service provider so the whole app inherits them. This is the cheapest safeguard against regressions introduced by ad-hoc XML generation in feature code.
How can I help you explore Laravel packages today?