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

Laravel Xml Converter Laravel Package

noki/laravel-xml-converter

A lightweight Laravel package for converting XML to arrays and arrays or objects back to XML. It offers configurable handling for attributes, namespaces, and formatting, making XML integration simple in Laravel apps.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm the surface area before writing any feature code. This is a thin, single-purpose package, so the first task is recon, not integration.

composer require noki/laravel-xml-converter

Then, in order:

  1. Check auto-discovery. Open vendor/noki/laravel-xml-converter/composer.json and look for extra.laravel.providers / aliases. If absent, register the provider manually — bootstrap/providers.php on Laravel 11+, or config/app.php on 10 and below.
  2. Look for publishable config. Most converter wrappers ship encoding/root-node defaults:
    php artisan vendor:publish --provider="Noki\\XmlConverter\\XmlConverterServiceProvider"
    
    If the command lists nothing, there is no config to publish — move on, don't hunt for it.
  3. Read vendor/noki/laravel-xml-converter/src before the README. With 32 stars and a single maintainer, the source is the documentation. You want two facts up front:
    • Which parser it wraps (SimpleXML vs DOMDocument vs XMLReader) — this predicts namespace, CDATA, and memory behaviour.
    • The exact public method names (toArray / convert / xmlToArray / toXml / arrayToXml). The entire API fits on one screen; confirm it rather than guessing.

First use case — parse an inbound XML payload into something Laravel already understands. The dominant real-world need is turning a partner webhook, SOAP response, or supplier feed into an array you can validate and persist:

use Noki\XmlConverter\Facades\XmlConverter; // confirm FQCN in src/

$xml  = $request->getContent();
$data = XmlConverter::toArray($xml);

$validated = validator($data['order'] ?? [], [
    'id'    => 'required|string',
    'total' => 'required|numeric',
])->validate();

Order::create($validated);

Second use case — emit XML. The inverse covers sitemaps, invoice exports, and legacy API responses:

$xml = XmlConverter::toXml(['root' => $order->toArray()]);

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

If the method names differ from the above, grep public function in the package source and adapt — don't build abstraction layers around method names you haven't confirmed.


Implementation Patterns

Normalise XML at the middleware boundary

Laravel's request pipeline assumes form or JSON bodies; XML arrives as an opaque string, so $request->input() and Form Requests see nothing. Parse once, centrally, rather than in every controller:

// app/Http/Middleware/ParseXmlBody.php
public function handle(Request $request, Closure $next)
{
    $contentType = $request->header('Content-Type', '');

    if (str_contains($contentType, 'xml')) {
        try {
            $request->merge(XmlConverter::toArray($request->getContent()));
        } catch (\Throwable $e) {
            report($e);
            abort(422, 'Malformed XML payload.');
        }
    }

    return $next($request);
}

After this, XML endpoints behave exactly like JSON endpoints — validation, $request->input(), and Form Requests all work unmodified. This is the single highest-leverage integration point.

Isolate the package behind your own contract

Given zero dependents and one maintainer, do not let the facade leak across the codebase. Bind one thin interface:

// app/Contracts/XmlEncoder.php
interface XmlEncoder
{
    public function parse(string $xml): array;
    public function encode(array $data, string $root = 'root'): string;
}
// app/Providers/AppServiceProvider.php
$this->app->bind(XmlEncoder::class, fn () => new class implements XmlEncoder {
    public function parse(string $xml): array
    {
        return XmlConverter::toArray($xml);
    }

    public function encode(array $data, string $root = 'root'): string
    {
        return XmlConverter::toXml([$root => $data]);
    }
});

Swapping to symfony/serializer or a native DOMDocument implementation later becomes a one-file change rather than a grep across forty controllers. With a package this small, you are the maintainer — write it so future-you can replace it.

Map arrays into DTOs explicitly, never hydrate blindly

The converter gives you a nested associative array of strings. Treat it as raw input, not domain state:

$data = $encoder->parse($xml);

$order = new OrderData(
    id:        (string) $data['order']['id'],
    total:     (float)  $data['order']['total'],
    createdAt: Carbon::parse($data['order']['created_at']),
    lines:     collect(Arr::wrap($data['order']['line'] ?? []))->map(
        fn ($line) => new OrderLineData(
            sku:      (string) $line['sku'],
            quantity: (int)    $line['quantity'],
        )
    )->all(),
);

Note the Arr::wrap — the converter cannot know whether a single <line> or many will appear, so it typically returns either a scalar-shaped array or a list. Wrapping is the defensive form that survives both cases.

Process larger payloads in jobs, not in the request cycle

Anything over a few hundred kilobytes belongs in a queued job with a size cap and a timeout:

// app/Jobs/ImportSupplierFeed.php
public int $timeout = 120;

public function handle(XmlEncoder $encoder): void
{
    $xml = Storage::disk('feeds')->get($this->path);

    if (strlen($xml) > 5 * 1024 * 1024) {
        throw new \RuntimeException("Feed exceeds 5MB in-memory limit.");
    }

    $rows = $encoder->parse($xml)['catalog']['product'] ?? [];

    collect(Arr::wrap($rows))->chunk(500)->each(
        fn ($chunk) => Product::upsert(
            collect($chunk)->map(fn ($r) => [
                'sku'   => $r['@sku'] ?? $r['sku'],
                'price' => (float) $r['price'],
            ])->all(),
            ['sku'],
            ['price'],
        )
    );
}

Chunk on the array side — you are already in memory, so batching saves DB round-trips, not parse memory.

Generate outbound XML from a known-shape array

For feeds, sitemaps, and exports, build a plain array and hand it to the encoder:

$feed = [
    'urlset' => [
        '@xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9',
        'url'    => $posts->map(fn ($post) => [
            'loc'        => route('posts.show', $post),
            'lastmod'    => $post->updated_at->toAtomString(),
            'changefreq' => 'weekly',
        ])->all(),
    ],
];

return response($encoder->encode($feed['urlset'], 'urlset'), 200, [
    'Content-Type' => 'application/xml',
]);

Keep the array shape close to the wire format — do not try to bend a rich domain model into the converter. Assemble DTOs or arrays first, then serialise.

Fixture-driven tests around the converter boundary

Because upstream test coverage is thin and unverified, own the contract:

public function test_parses_a_partner_order(): void
{
    $xml  = file_get_contents(base_path('tests/fixtures/order.xml'));
    $data = app(XmlEncoder::class)->parse($xml);

    $this->assertSame('ORD-10423', $data['order']['id']);

    // Guard against silent regressions on the repeated-node shape
    $this->assertIsArray(Arr::wrap($data['order']['line']));
}

Store a real payload from the actual partner, not a synthetic one — the edge cases that bite are the ones you did not invent.


Gotchas and Tips

Verify parser hardening — do not assume it

The PM and TPM assessments both flag XXE and entity expansion as blockers pending proof, and rightly so. Before this touches any internet-facing endpoint, confirm the underlying parser disables external entity loading:

// What you want to see in the package source (DOMDocument):
$dom = new \DOMDocument();
$dom->loadXML($xml, LIBXML_NONET | LIBXML_NOENT ^ LIBXML_NOENT); // entities off
// and, on PHP 8+, no call to libxml_disable_entity_loader (deprecated) but
// explicit absence of LIBXML_DTDLOAD / LIBXML_NOENT

If the source does not explicitly set flags, wrap the call and add a defensive check of your own: strip <!DOCTYPE and <!ENTITY from untrusted input, or reject payloads containing them outright. For regulated or internet-facing ingestion, treat this as a blocker until proven.

The array conversion is lossy — plan for it

Array round-trips cannot preserve everything XML can express. Expect to lose or flatten:

  • Namespaces. Element and attribute prefixes are commonly dropped or merged into the key. If your payload mixes namespaces (e.g. a SOAP envelope with a soap: body and a namespace-less payload), inspect the output before building on it.
  • Attributes vs child elements. A common convention is @attr for attributes and plain keys for elements — but if an element has both an attribute and a child of the same name, one may shadow the other. Check your real payload.
  • Repeated elements. A single <item> may come back as an array; two items as a list of arrays; zero items as absent. Always Arr::wrap() before iterating.
  • Mixed content and CDATA. Text interleaved with elements, and CDATA sections, are frequently collapsed to their text content — the structure disappears.
  • Ordering. Associative arrays preserve insertion order in PHP, but if the converter sorts keys, round-tripping will reorder nodes. Test with a payload where order matters (e.g. schema-validated output).

Everything is a string

toArray() will not cast "3" to 3 or "true" to true. Cast explicitly at the DTO boundary, and never feed the raw array straight into a numeric validation rule without casting first — numeric on "1,234" will fail, and integer on "03" may surprise you.

Encoding and BOM

The common failure mode is a UTF-8 file with a BOM or a declared encoding="ISO-8859-1" that mismatches the actual bytes. Symptoms are SimpleXMLElement::__construct(): Entity: line 1: parser error at offset 0, or mojibake in a single field. Handle by normalising before parse:

$xml = ltrim($xml, "\xEF\xBB\xBF");           // strip UTF-8 BOM
if (! mb_check_encoding($xml, 'UTF-8')) {
    $xml = mb_convert_encoding($xml, 'UTF-8', 'ISO-8859-1');
}

Also watch whitespace-only text nodes: pretty-printed input can produce spurious empty-string keys, so trim the payload with preg_replace('/>\s+/', '>', $xml) if you see unexpected keys.

Root node handling on outbound XML

toXml(['root' => $data]) and toXml($data, root: 'root') are easy to confuse. Test the actual output for the root tag, XML declaration, and xmlns attribute you expect — a missing declaration or a doubled root is the most common silent bug when generating a response. Sniff it in a test:

$this->assertStringStartsWith('<?xml', $xml);
$this->assertStringContainsString('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">', $xml);

Size limits must be yours

The converter loads the whole document into memory; there is no streaming surface. Set an explicit byte cap on all untrusted input before calling toArray(). Without one, a 200MB "statement" from a partner becomes a fatal Allowed memory size exhausted in the queue worker at 3am. For anything larger, switch to XMLReader with a pull-parser loop — the package is the wrong tool there and no amount of chunking will fix it.

Debugging checklist

When the output looks wrong, work down this list before touching the package source:

  1. Print the raw XML. Log $request->getContent() on the failing endpoint — you are often testing a payload that was already consumed or mangled upstream.
  2. Validate it. xmllint --noout payload.xml catches well-formedness problems the converter will simply throw on.
  3. Compare against SimpleXML. A ten-line script dumping simplexml_load_string($xml) alongside you toArray() output tells you whether the divergence is the parser or the converter.
  4. Diff the fixture. Keep a gold-master array of a real payload and assertSame against it; regressions in namespace or repeated-node handling will surface immediately.

Pin, vendor, and own it

With 32 stars, zero dependents, and a single maintainer, expect the upstream to go quiet. In practice:

"require": { "noki/laravel-xml-converter": "1.*" }

Pin the major, run composer audit in CI, and keep the adapter from the Implementation Patterns section so a replacement is a one-file change. If the package is final on the facade and you need behaviour it lacks — a custom attribute convention, a root-node option, entity hardening — fork it into app/Support/Xml and own the code. The MIT licence makes that trivial, and at this size a fork is cheaper than waiting for a PR.

Extension points worth checking for

Before assuming it is closed, look in src/ for a config-driven mapper or a parser-configurable constructor. Several thin converters expose a $depth, $options, or $parserFlags argument — if noki does, that is your hook for hardening DOMDocument flags without forking. If it doesn't, the wrapper is the extension point.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin