saloonphp/xml-wrangler
XML Wrangler is a Saloon plugin that makes working with XML APIs painless. It adds XML request bodies, automatic XML responses parsing, and convenient helpers for converting between arrays and XML so you can focus on building integrations, not boilerplate.
Installation:
composer require saloonphp/xml-wrangler
Ensure your project uses PHP 8.1+ (required for generics and type safety).
First Use Case: Parse an XML string into a typed array/collection:
use Saloon\XmlWrangler\XmlReader;
$xml = '<root><user id="123"><name>John</name></user></root>';
$data = XmlReader::fromString($xml)
->query('//user')
->first()?->getAttribute('id'); // Returns "123"
Key Entry Points:
XmlReader: Parse XML into traversable nodes (arrays/objects).XmlWriter: Generate XML from arrays/objects.Query: XPath-like querying with Laravel Collection methods (e.g., map, filter).Where to Look First:
tests/ folder: Real-world examples of parsing/writing XML.src/Query.php: Core querying logic (e.g., XPath, attribute access).Use XmlReader in Saloon connectors to normalize XML responses:
use Saloon\Connector;
use Saloon\XmlWrangler\XmlReader;
class PaymentGatewayConnector extends Connector
{
public function resolve(): array
{
return [
'base_uri' => 'https://api.gateway.com',
'response_object' => XmlResponse::class,
];
}
}
class XmlResponse extends Response
{
public function parse(): array
{
return XmlReader::fromString($this->body)
->query('//Transaction')
->map(fn ($node) => [
'id' => $node->getAttribute('id'),
'status' => $node->getContent(),
])
->toArray();
}
}
Convert arrays/DTOs to XML for SOAP/REST+XML APIs:
use Saloon\XmlWrangler\XmlWriter;
$xml = XmlWriter::make()
->element('Envelope')
->element('Body')
->element('PurchaseOrder', [
'orderId' => 'PO123',
'date' => date('Y-m-d'),
])
->element('Items')
->element('Item', ['sku' => 'SKU456'], 'Quantity: 2')
->up()
->up()
->toString();
Leverage Laravel Collection methods on XML nodes:
$nodes = XmlReader::fromString($xml)
->query('//Product')
->filter(fn ($node) => $node->getAttribute('price') > 100)
->pluck('name'); // Returns Collection of product names
Map namespaces for XPath queries:
$reader = XmlReader::fromString($xml)
->mapNamespace('ns', 'http://example.com/ns');
$nodes = $reader->query('//ns:Order'); // Queries namespaced elements
Process files >10MB without loading into memory:
$reader = XmlReader::fromFile('large_file.xml', stream: true);
foreach ($reader->query('//Record') as $node) {
// Process each node incrementally
}
Use generics to enforce return types:
use Saloon\XmlWrangler\Query;
function getUserData(string $xml): array
{
return XmlReader::fromString($xml)
->query('//User')
->first()?->getAttributes(); // Returns ?array
}
Mock XML responses in tests:
use Saloon\Testing\Mock;
$mock = Mock::soap()
->withRequestMatching('//PurchaseOrder')
->withResponseFromFile(__DIR__.'/fixtures/response.xml');
$response = $this->connector->send(new PurchaseOrderRequest());
$orders = $response->parse(); // Uses XmlReader under the hood
Namespace Quirks:
// ❌ Fails silently if namespace isn't mapped
$reader->query('//ns:Order');
$reader->mapNamespace('ns', 'http://example.com/ns')->query('//ns:Order');
Streaming Mode Pitfalls:
// ❌ Throws "Stream position already read"
$reader = XmlReader::fromFile('file.xml', stream: true);
$reader->query('//Node1'); // Consumes stream
$reader->query('//Node2'); // Fails
XPath Edge Cases:
.//Node) behave differently than absolute paths (//Node).//) for consistency:
// Prefer:
$reader->query('//User/Address');
// Over:
$reader->query('.//Address'); // May fail if context is wrong
Attribute vs. Content Confusion:
getAttribute() vs. getContent() are easy to mix up:
// ❌ Returns empty string (looks for attribute "name")
$node->getContent('name');
// ✅ Correct:
$node->getAttribute('name'); // For attributes
$node->getContent(); // For text content
PHP 8.1+ Generics:
// ❌ May throw type error in PHP 8.2+
public function parse(): array { ... }
// ✅ Explicit nullable:
public function parse(): ?array { ... }
Malformed XML:
veezee/xml for validation:
// Add to your pipeline:
$xml = XmlReader::fromString($rawXml);
$validator = new \Veewee\Xml\Validator();
$validator->validate($xml->toString());
Inspect Raw XML:
Use toString() to debug:
$writer = XmlWriter::make()->element('Root');
dump($writer->toString()); // Visualize generated XML
XPath Testing: Test queries in isolation:
$reader = XmlReader::fromString($xml);
dump($reader->query('//*')->toArray()); // List all nodes
Stream Position: Reset streams for debugging:
$stream = fopen('file.xml', 'r');
rewind($stream);
$reader = XmlReader::fromStream($stream);
Performance:
$users = $reader->query('//User')->cache();
$activeUsers = $users->filter(...);
Custom Node Classes:
Extend XmlNode to add domain-specific methods:
class UserNode extends XmlNode
{
public function getFullName(): string
{
return $this->query('./FirstName')->getContent() . ' ' .
$this->query('./LastName')->getContent();
}
}
Query Macros: Add reusable query logic:
Query::macro('byStatus', function (string $status) {
return $this->filter(fn ($node) =>
$node->getAttribute('status') === $status
);
});
// Usage:
$reader->query('//Order')->byStatus('shipped');
Writer Customization: Override default XML generation:
$writer = XmlWriter::make()
->setEncoding('UTF-8')
->setStandalone(true);
Integration with Saloon:
Create a base XmlResponse class for all connectors:
abstract class XmlResponse extends Response
{
public function parse(): array
{
return XmlReader::fromString($this->body)
->query($this->
How can I help you explore Laravel packages today?