saloonphp/xml-wrangler
XML helper for Saloon: parse XML responses into arrays/objects, map nodes to data, handle namespaces, attributes and CDATA, and build or transform XML payloads cleanly. Great for SOAP-style APIs and legacy XML integrations in Laravel/PHP.
Installation:
composer require saloonphp/xml-wrangler
First Use Case: Parsing XML Responses Parse XML from a Saloon HTTP response:
use SaloonHttp\Connectors\Connector;
use SaloonPhp\XmlWrangler\XmlReader;
class ApiConnector extends Connector
{
public function resolve(): array
{
$xml = XmlReader::fromResponse($this->response);
return $xml->query('//data/item')->map(fn($el) => [
'id' => $el->getAttribute('id'),
'name' => $el->getContent(),
])->all();
}
}
First Use Case: Generating XML Requests Create XML payloads for API requests:
use SaloonPhp\XmlWrangler\XmlWriter;
$writer = XmlWriter::new()
->element('request', fn($w) => $w
->attribute('version', '1.0')
->element('user', fn($w) => $w
->attribute('id', '123')
->element('name', 'John Doe')
)
);
$this->request->withBody($writer->toXmlString());
Where to Look First:
XmlReader (parsing) and XmlWriter (generation).Query for DTOs (PHP 8.1+).Parse XML responses in Saloon connectors with type safety:
use SaloonHttp\Connectors\Connector;
use SaloonPhp\XmlWrangler\Query;
class PaymentGatewayConnector extends Connector
{
public function resolve(): array
{
$xml = XmlReader::fromResponse($this->response);
return Query::fromXml($xml->query('//payments/payment'))
->map(fn($el) => [
'transaction_id' => $el->getAttribute('id'),
'amount' => (float) $el->getContent('amount'),
'status' => $el->getContent('status'),
])
->all();
}
}
Generate XML requests from Laravel models:
use SaloonPhp\XmlWrangler\XmlWriter;
use App\Models\User;
$user = User::find(1);
$writer = XmlWriter::new()
->element('user_update', fn($w) => $w
->attribute('id', $user->id)
->element('email', $user->email)
->element('status', $user->status)
);
$this->request->withBody($writer->toXmlString());
Work with namespaced XML (e.g., SOAP APIs):
$reader = XmlReader::fromResponse($this->response)
->withNamespaces([
'soap' => 'http://schemas.xmlsoap.org/soap/envelope/',
'ns' => 'http://example.com/ns',
]);
$response = $reader->query('//soap:Envelope/soap:Body/ns:response')->first();
$status = $response->getContent('status');
Use generics for type-safe XML parsing (PHP 8.1+):
use SaloonPhp\XmlWrangler\Query;
class PaymentDto {
public function __construct(
public string $transactionId,
public float $amount,
public string $status,
) {}
}
$payments = Query::fromXml<PaymentDto>($reader->query('//payments/payment'))
->map(fn($el) => new PaymentDto(
transactionId: $el->getAttribute('id'),
amount: (float) $el->getContent('amount'),
status: $el->getContent('status'),
))
->all();
Process large XML responses incrementally:
$reader = XmlReader::fromResponse($this->response);
$reader->query('//records/record')->each(fn($el) => {
$this->dispatch(new ProcessRecord($el->getContent('data')));
});
Bind XmlReader/XmlWriter for dependency injection:
// app/Providers/AppServiceProvider.php
$this->app->bind(XmlReader::class, function ($app) {
return XmlReader::fromResponse($app['saloon']->response);
});
Use snapshot testing for XML generation:
it('generates correct XML', function () {
$writer = XmlWriter::new()
->element('invoice', fn($w) => $w
->attribute('id', 'INV-123')
->element('total', 100.00)
);
expect($writer->toXmlString())->toMatchSnapshot();
});
Extend XmlReader for domain-specific logic:
class PaymentXmlReader extends XmlReader
{
public function findByTransactionId(string $id): ?XmlElement
{
return $this->query("//payment[@transaction_id='$id']")->first();
}
}
Convert XML to Laravel Collections:
use Illuminate\Support\Collection;
$reader = XmlReader::fromResponse($this->response);
$collection = collect($reader->query('//items/item')->map(fn($el) => [
'id' => $el->getAttribute('id'),
'name' => $el->getContent('name'),
'price' => (float) $el->getContent('price'),
])->all());
Save XML output to files or return in HTTP responses:
// Save to file
file_put_contents('export.xml', $writer->toXmlString());
// Return in Saloon response
$this->response->withBody($writer->toXmlString());
Handle malformed XML gracefully:
try {
$reader = XmlReader::fromResponse($this->response);
// Parse logic
} catch (\SaloonPhp\XmlWrangler\Exceptions\XmlException $e) {
$this->response->withStatus(400)->withBody('Invalid XML: ' . $e->getMessage());
}
Enable pretty printing for readability:
$writer = XmlWriter::new()->prettyPrint();
$writer->element('root', fn($w) => $w
->element('item', 'Test')
);
Null Checks for first()/sole():
Always validate results to avoid NullPointerException:
if ($element = $reader->query('//item')->first()) {
$name = $element->getContent('name'); // Safe
}
XPath Syntax Errors:
//child for any-depth searches.@attr='value&amp;').Stream Positioning: Rewind streams before parsing:
$reader = XmlReader::fromResponse($this->response)->rewind();
PHP 8.4+ Nullable Types: Handle nullable returns explicitly:
$content = $el->getContent() ?? 'default';
Namespace Conflicts: Ensure namespace prefixes match the XML document:
$reader->withNamespaces(['ns' => 'http://example.com/ns']);
Case Sensitivity in XPath:
XPath is case-sensitive by default. Use local-name() for case-insensitive queries:
$reader->query("//*[local-name()='Item']");
Memory Issues with Large XML:
Use each() for lazy evaluation instead of all():
$reader->query('//records')->each(fn($el) => {
// Process one record at a time
});
Saloon Response Body Handling: Ensure the response body is a string before parsing:
if (!$this->response->isSuccessful() || !$this->response->hasBody()) {
throw new \RuntimeException('Invalid response');
}
$xml = XmlReader::load((string) $this
How can I help you explore Laravel packages today?