simonschaufi/pretty-xml
A tiny PHP library for pretty printing and minifying XML with configurable indent size and character, since DomDocument can't configure indent distance. Install via Composer: simonschaufi/pretty-xml.
Architecture fit
simonschaufi/pretty-xml is a single-purpose string transformer: a PrettyXml\Formatter object with format(), minify(), and two setters for indent size and character. It sits in the presentation/debug layer, not the domain layer, so it does not touch business logic, persistence, or contracts — the lowest-blast-radius category of dependency.
DOMDocument normalizes and re-indents output but exposes no indent-width control. This package exists solely to close that gap. If we have any XML-emitting surface (SOAP clients, legacy partner integrations, sitemaps, RSS/Atom feeds, export endpoints), we have this gap today, usually papered over with preg_replace or "good enough" saveXML() output.setIndentSize, setIndentCharacter). Binding it as a Laravel singleton and mutating it per call site invites configuration bleed — catastrophically so under Octane or any long-lived worker, where one request's setIndentSize(2) becomes the next request's default. Wrap it behind a small XmlFormatter interface resolving a fresh Formatter per invocation, or an immutable config-bound factory.format(), not prettyPrint(). Any design doc referencing a static PrettyXml::prettyPrint() facade describes a wrapper we would have to build, not the package. Worth correcting before it propagates into code.Integration feasibility
Straightforward. One composer require, no framework coupling, no service provider, no config file, no facade. The realistic engineering cost is measured in hours, not days, and most of it is wrapper plus tests, not adoption.
format(format($x)) === format($x) as a regression assertion is cheap and locks in the 3.1.1 behavior.Technical risk
The technical risk is low; the organizational risk is that we over-invest in a 22-star utility and treat it as infrastructure.
DOMDocument/SimpleXMLElement has already accepted.<p>Hello <b>world</b></p>), or where downstream systems are whitespace-sensitive, is at risk. Release 3.1.1 ("reach a fixpoint when formatting text nodes") tells us successive formatting passes previously drifted — that is precisely the defect profile that bites stacked middleware, cache-then-reformat paths, and queued re-processing.^3.1.1. Taking ^3.0 reintroduces non-convergent formatting, and 3.0.0 was itself a full algorithm rewrite — two consecutive majors touching the core algorithm is a signal that output stability is still maturing.Key questions
^3.1.1 is a hard floor.Stack fit
Native PHP/Laravel. No stack change, no runtime capability required, no infrastructure implication. It complements rather than competes with existing tooling: Response::xml() for emission, SimpleXMLElement/DOMDocument for parsing and validation, PHPUnit/Laravel XMLAssertions for verification. The package occupies only the "string → prettier string" slot.
Migration path
App\Support\XmlFormatter (interface plus a PrettyXmlFormatter implementation that instantiates a fresh Formatter per call and applies indent config from config/xml.php). This is the entire migration insurance policy.saveXML, asXML, preg_replace on XML, and response()->xml. Pilot on one non-critical, well-formed, non-whitespace-sensitive endpoint; diff before/after output byte-for-byte.application/xml/text/xml content types, with an explicit re-entry guard (response header or attribute). 3.1.1 makes re-entry safe, but the guard keeps intent legible to the next engineer.production, pretty-print in local/staging unless a caller opts in. This eliminates the performance and whitespace risk from the production path entirely.assertXmlStringEqualsXmlString() for equivalence, plus format(format($x)) === format($x) for idempotency. Run the same fixture set against representative partner payloads.Compatibility
DOMDocument::saveXML(), and SimpleXMLElement::asXML() output — those last two are the natural call sites.minify($xml, true) is the opt-in for comments). Any schema depending on exact original indentation is out of scope.Sequencing
| Step | Priority | Effort | Gate / Dependency |
|---|---|---|---|
Verify package metadata and Pin ^3.1.1 |
High | Low | Packagist check |
Build XmlFormatter wrapper + fresh-instance-per-call |
High | Low | None |
| Inventory XML-emitting surfaces | High | Low | None |
| Idempotency + equivalence regression tests | High | Low | Wrapper |
| Pilot on one non-critical endpoint | Medium | Low | Inventory |
| Middleware rollout, env-gated | Medium | Medium | Pilot diff clean |
| Whitespace-sensitive payload review | High | Medium | Integration owners |
| Performance benchmark >1 MB | Medium | Low | Pilot |
| Team enablement + wiki | Low | Low | Rollout |
Note the ordering: the whitespace review gates rollout, not the pilot or the wrapper. Do not serialize everything behind it.
Maintenance
composer update. Budget realistically at a few hours per quarter, dominated by "is it still maintained?" checks rather than code changes.^3.1.1 in composer.json, not just the lockfile, so lockfile regeneration cannot silently walk back the idempotency fix.Support
format() vs minify(), the fresh-instance rule, the environment gating, and the explicit warning that this does not validate XML.pretty-xml tag plus a note in the XML/integration runbook so the next person debugging "why did our XML output change" finds the cause in minutes rather than hours.Scaling
Failure modes
| Scenario | Impact | Mitigation |
|---|---|---|
Mutable Formatter bound as singleton, config set per call site |
Cross-request config bleed (severe under Octane); output inconsistency | Fresh instance per call; never mutate a shared instance |
| Formatting applied at multiple layers | Output drift / non-convergence on pre-3.1.1 | Pin ^3.1.1; still gate re-entry explicitly |
| Whitespace-sensitive or mixed-content payload reformatted | Downstream contract breach, silent | Restrict to known-safe endpoints; prefer minify(); review with integration owners |
| Malformed XML passed to formatter | Garbled output, no error raised | Validate upstream with DOMDocument before formatting |
| Performance regression on large payloads | Latency spike on XML endpoints | Benchmark >1 MB; env-gate pretty-print; cache minified output |
| Package abandoned or PHP-incompatible release | Stalled upgrades | Wrapper interface + MIT fork/vendor option |
Doc examples drift from real API (prettyPrint doesn't exist) |
Wasted engineering hours, broken snippets | Wrapper owns the vocabulary; correct internal docs before rollout |
| Unverified release metadata (future-dated release) | Wrong pin decision | Confirm on Packagist before shipping the constraint |
Ramp-up
format() vs minify(), the wrapper's fresh-instance rule, environment gating, and the explicit "not a validator" boundary.assertXmlStringEqualsXmlString() equivalence checks plus the double-format idempotency assertion, and a fixture set drawn from real partner payloads — not synthetic ones, since the failure mode is whitespace in production data.>= 3.1.1, and re-survey after two weeks. If after a sprint the number of call sites is one or two, that is a legitimate outcome — the correct conclusion is "this solved a small problem cheaply," not "we should have built a platform around it."How can I help you explore Laravel packages today?