Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Fluidxml Laravel Package

servo/fluidxml

FluidXML is a lightweight PHP library for creating, querying, and editing XML with a concise, fluent, jQuery-like API. Chain readable methods, target nodes with XPath selectors, handle namespaces, and cut boilerplate in XML-heavy PHP and Laravel applications.

View on GitHub
Deep Wiki
Context7

Getting Started

FluidXML is a dependency-free, single-class library — no service provider, no config file, no facade. Install it and you're done:

composer require servo/fluidxml

Where to look first

  • The README on GitHub — it is essentially the whole API surface in one page.
  • The global fluidxml() helper (registered via Composer's files autoload). If you prefer DI, use new Servo\FluidXml\FluidXml(...) directly.
  • The four methods that matter in 90% of usage: add(), set(), attr(), and query() (plus xml()/html() for output).

First use case — a sitemap route

The most common Laravel need for XML is a sitemap. In routes/web.php:

use function Servo\FluidXml\fluidxml;

Route::get('/sitemap.xml', function () {
    $xml = fluidxml('urlset')
        ->namespace('http://www.sitemaps.org/schemas/sitemap/0.9');

    Post::published()->lazy()->each(function ($post) use ($xml) {
        $xml->add('url')
            ->add('loc', route('posts.show', $post))
            ->add('lastmod', $post->updated_at->toAtomString())
            ->add('changefreq', 'weekly');
    });

    return response($xml->xml(), 200, ['Content-Type' => 'application/xml']);
});

If that renders valid XML and passes assertXmlStringEqualsXmlString(), you understand the core mental model: every builder method returns a FluidXML instance for chaining, and the instance itself holds a mutable DOM tree.

Implementation Patterns

Wrap it in a service. Don't scatter fluidxml() calls through controllers. Create App\Services\Xml\SitemapBuilder or AtomFeedBuilder with typed methods (addEntry(Post $post): self) and resolve it from the container. This gives you one place to swap in an interface if you ever migrate to spatie/laravel-sitemap or similar. It also makes the generator unit-testable without HTTP.

Large exports — stream, don't buffer. FluidXML keeps the whole DOM in memory. For big sitemaps or product feeds, build up chunks and stream with response()->streamDownload(), flushing every N nodes:

return response()->streamDownload(function () use ($query) {
    echo '<?xml version="1.0" encoding="UTF-8"?><urlset>';
    $query->chunk(1000, function ($rows) {
        $chunk = fluidxml('urlset')->namespace('http://www.sitemaps.org/schemas/sitemap/0.9');
        foreach ($rows as $row) { /* ->add('url', ...) */ }
        echo $chunk->xml(['strip_declaration' => true]);
    });
    echo '</urlset>';
}, 'sitemap.xml', ['Content-Type' => 'application/xml']);

Alternatively use ->save('php://output') per chunk.

SOAP / XML APIs. The pattern that fits Laravel best is: build the envelope with FluidXML → send via Http::withBody($xml->xml(), 'text/xml')->post($endpoint) → parse the response with fluidxml($response->body())->query('//soap:Body/...'). Register namespaces before querying:

$envelope = fluidxml('Envelope')->namespace('soap', 'http://schemas.xmlsoap.org/soap/envelope/');

Eloquent → XML for partners/ERP. Use chunk() or lazy() combined with a fresh fluidxml() per chunk, and write to Storage::disk('s3')->append() or a local temp file. Avoid ->get() on large tables.

Interop with existing XML. fluidxml($dom), fluidxml($simpleXml), fluidxml($string) and fluidxml('existing.xml') all load. This is how you graft new nodes into an XML template (e.g., an Invoice XML schema) — load the skeleton, query for the insertion point, ->add() children, output.

Mutations on selections. append(), prepend(), remove(), add() on a query() result operate on every matched node. This makes bulk edits (e.g., stripping a <private> element from all entries in an OAI-PMH response) a one-liner.

Blade for the skeleton, FluidXML for the loops. A common hybrid: put the header/doctype in a Blade view, pass each chunk of generated XML as {!! $chunk !!}. Avoids the "how do I build namespaced attributes through Blade" pain while keeping rendering fast.

Testing. Wrap assertions in PHPUnit's assertXmlStringEqualsXmlString($expected, $actual) rather than string comparison — attribute ordering and whitespace will bite you otherwise. For feeds, keep one checked-in fixture of expected XML.

Gotchas and Tips

  • No framework glue. FluidXML has zero Laravel-isms: no facade, no config, no service provider, and effectively zero dependents listed. You're responsible for conventions, testing strategy, and upgrades. Treat it as a small utility, not infrastructure.
  • The global fluidxml() helper is autoloaded via Composer files. If you've already defined a function with that name (or a package does), you get a fatal collision. Wrap: use function Servo\FluidXml\fluidxml as fx; only works within a file, not globally. Safest is to instantiate new FluidXml() inside your service.
  • XPath + namespaces is the #1 source of "why is my query empty". You must register the prefix on the same FluidXML instance before querying, and the prefix in the XPath must match. For documents with a default namespace (sitemaps, SOAP, SVG), every element inherits it — use ->namespace('d', 'http://...') and query //d:url. This bites people coming from DOMXPath::registerNamespace patterns where they assume default-namespace queries just work.
  • add() vs set() vs addChild(). add() always creates a new child and returns that child as context. set() ensures a single node exists with the given value (updates if present). Mixing them mid-chain is how you end up with duplicate <loc> tags. Rule of thumb: add() for repeated elements, set() for singletons.
  • Context changes silently. Because add() returns the new child, chaining ->add('url')->add('loc', ...)->add('lastmod', ...) adds the dates to <url>, not <urlset>. Great for fluency, dangerous when you assume you're still on the parent. Use a second variable when you need to escape the current context.
  • Attributes are the second argument or ->attr(). ->add('x', ['id' => 1, 'class' => 'y']) sets those as attributes on <x>. ->attr('id', 1) on a selection sets attributes on all matched nodes.
  • Output methods differ. ->xml() emits <?xml version="1.0" ...?> + body. ->html() emits an HTML doctype. When you nest one FluidXML inside another via append()/add($child), the child won't be re-declared — good. But if you call ->xml() on the child and echo it manually, you'll get a second declaration and an invalid document. Strip it or use ->save('php://output').
  • ->save($path) uses PHP's filesystem directly. In Laravel this bypasses Storage disks, local-dev/ prod path differences, and Flysystem's atomic writes. Prefer Storage::disk(...)->put($path, $xml->xml()) (or ->append() per chunk) for portability and testability.
  • Memory ceiling. A 50,000-URL sitemap with metadata is comfortably under the spec's 50MB serialized limit but can easily consume 300–500MB of DOM memory. Either chunk-and-stream, or use DOMDocument only for validation while streaming output. Test against 100k records before shipping.
  • Error handling. Malformed input to fluidxml($str) throws — this is a hard failure path when calling third-party APIs. Wrap parses in try/catch and log the raw response before rethrowing; you will get HTML error pages from misbehaving endpoints on a bad day.
  • Version drift. Last tagged release was Nov 2023, and the 2.x API is quietly incompatible with a lot of 1.x snippets still ranking on Google ($xml->setAttribute(), $xml->addElement(), etc. don't exist anymore). Read the README against the version you actually install and pin a specific constraint in composer.json.
  • Extension points are minimal but real. You can subclass FluidXml to add domain helpers (e.g., ->addProduct(Product $p)), or wrap it in a value object. There's no event/hook system, so composition (a FeedBuilder owning a FluidXml) is cleaner than inheritance.
  • Testing pitfalls. DOM serialization is not deterministic across PHP versions for attribute ordering in some edge cases; write assertions against assertXmlStringEqualsXmlString, and if you snapshot XML, normalize whitespace first. For feeds that include timestamps, Carbon::setTestNow() combined with a fixed time makes snapshots viable.
  • PHP 8.4+ watch. The package hasn't been updated since late 2023; if you run bleeding-edge PHP, run your test suite and check for deprecation notices before upgrading — there's no upstream fix pipeline to rely on.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource