xml_decode()

Turn an XML string into a nested array. Use it for RSS and Atom feeds, and for APIs that answer in XML instead of JSON.

It is the XML counterpart to fromjson(). The name is xml_decode because xml_parse is already taken by a different, low-level PHP function.

Syntax

xml_decode(xml, list_tags)

Parameters

Name Type Required Description
xml string yes The XML text, for example the body of a curl_get() response.
list_tags string or array no Names of text-only tags that should always be a list. Only needed for the case described in "Text tags that repeat" below.

Returns

A nested array, or null if the text is empty or is not valid XML (the reason is written to the run log).

How elements map:

XML Result
<title>News</title> the string "News"
<empty/> an empty string
<entry>...</entry> (has children) a list, even if there is only one
<link href="http://x/"/> (has attributes) a list: ["@attributes" => ["href" => "http://x/"](/help/n-a)]
<content type="html">Hi</content> [["@attributes" => ["type" => "html"], "@text" => "Hi"]]
<media:thumbnail/> keyed by the full name, "media:thumbnail"

The outer element is unwrapped: for a feed whose root is <feed>, the array you get back is the contents of <feed>. Text is trimmed, and CDATA blocks are unwrapped for you.

Any element with children or attributes is a list

XML has no way to mark something as a list. A list is just the same tag written more than once, so a parser that counts occurrences hands you a different shape depending on how many results came back: your loop works all month, then a quiet week returns one item and the loop silently walks that item's own fields instead.

xml_decode() does not count. Any element that has children or attributes is always a list, so one result and fifty results read exactly the same:

$feed = xml_decode($resp["body"]);
foreach ( $feed["entry"] as $entry ) {
    sys_log($entry["title"]);
}

That is why the [0] appears on link below: it has an attribute, so it is a list of one.

Plain text elements stay plain, so titles, ids and dates read normally:

$entry["title"]                          // "Bending Spoons acquisition"
$entry["link"][0]["@attributes"]["href"] // "https://www.reddit.com/r/..."
$entry["author"][0]["name"]              // "/u/example"

Text tags that repeat

The one thing left to watch is a text-only tag that can appear more than once, such as <category>news</category> in RSS. Alone it is the string "news"; twice it becomes ["news", "tech"]. Name it in list_tags and it is always a list:

$feed = xml_decode($resp["body"], "category");
$feed = xml_decode($resp["body"], ["category", "keyword"]);   // several

Tags with attributes or children never need this.

Example

Read a Reddit search feed and create a record for each post:

$resp = curl_get("https://www.reddit.com/r/nocode/search.rss?q=airtable&restrict_sr=1&sort=new");
$feed = xml_decode($resp["body"]);

foreach ( $feed["entry"] as $entry ) {
    record_create("Mentions", [
        "title" => $entry["title"],
        "url" => $entry["link"][0]["@attributes"]["href"],
        "author" => $entry["author"][0]["name"],
        "posted" => date_to_utc($entry["updated"])
    ]);
}

Example output

[
  "title" => "search results",
  "updated" => "2026-08-22T00:00:00+00:00",
  "entry" => [
    [
      "title" => "Anyone else looking at alternatives?",
      "id" => "t3_abc",
      "link" => [ ["@attributes" => ["href" => "https://www.reddit.com/r/nocode/comments/abc/"]] ],
      "author" => [ ["name" => "/u/example"] ],
      "content" => [ ["@attributes" => ["type" => "html"], "@text" => "<p>...</p>"] ],
      "updated" => "2026-08-20T12:00:00+00:00"
    ]
  ]
]

RSS 2.0

RSS puts the posts one level down, inside the channel. The channel has children, so it is a list of one:

$feed = xml_decode($resp["body"]);
foreach ( $feed["channel"][0]["item"] as $item ) {
    sys_log($item["title"] . " " . $item["link"]);
}

Notes

  • Feeds often wrap HTML in the content or description element. Run it through strip_tags_gf() before storing it as plain text.
  • Feed dates are usually already UTC. Store them with date_to_utc() if the feed carries a local offset.
  • External entities and DTD references are ignored, so a hostile document cannot make your automation read files or fetch URLs.
  • api_request(), curl_request(), and the other HTTP functions already decode an XML response body into response["json"] when the server sends an XML content type. That decoding is older and uses a different, looser shape (single elements are not listed, and an attribute is dropped when the element also has text). If you want the shape described on this page, pass response["body"] to xml_decode() yourself.

See also: curl_get(), strip_tags_gf(), array_search_col()