1


I've been trying to merge two XML files I use to build my menubar in my web application for hours, but I can't get it to work.
I have my main XML file which looks like this:

<?xml version="1.0" encoding="ISO-8859-1" ?>
<root>
    <version>1.0.0</version>
    <menu>
        <Category1>
            <item>
                <id>Cake</id>
                <nr>1</nr>
                <hint>I like these</hint>
                <userlevel>5</userlevel>                
            </item>
            <item>
                <id>Cake 2</id>
                <nr>2</nr>
                <hint>I like these too, but only for me</hint>
                <userlevel>10</userlevel>               
            </item>
        <Category1>

        <Category2WithApples>
        <item>
            <id>Apple Cake</id>
            <nr>1</nr>
            <hint>Sweet</hint>
            <userlevel>5</userlevel>                
        </item>
        <item>
            <id>Rainbow Cake</id>
            <nr>2</nr>
            <hint>Mine!!</hint>
            <userlevel>10</userlevel>               
        </item>
        <Category2WithApples>
    </menu>
</root>

Now, I want each user to be able to load in his custom XML which is in the same folder as the main.xml which looks like this:

<CategoryMyOwn>
    <item>
        <id>Item in my Category</id>
        <nr>0</nr>
        <hint>Some text</hint>
        <userlevel>0</userlevel>                
    </item>
</CategoryMyOwn>
<Category1>
    <item>
        <id>Item in existing category</id>
        <nr>0</nr>
        <hint>Some text</hint>
        <userlevel>0</userlevel>                
    </item>
</Category1>    

I've tried solutions from

but they all do not work at all for me or just append the second file to the end of my main.xml. So, my question is, how do I properly merge the user.xml into my main.xml so it looks like this:

<?xml version="1.0" encoding="ISO-8859-1" ?>
<root>
    <version>1.0.0</version>
    <menu>
        <Category1>
            <item>
                <id>Cake</id>
                <nr>1</nr>
                <hint>I like these</hint>
                <userlevel>5</userlevel>                
            </item>
            <item>
                <id>Cake 2</id>
                <nr>2</nr>
                <hint>I like these too, but only for me</hint>
                <userlevel>10</userlevel>               
            </item>
            <item>
                <id>Item in existing category</id>
                <nr>0</nr>
                <hint>Some text</hint>
                <userlevel>0</userlevel>                
            </item>
        <Category1>

        <Category2WithApples>
        <item>
            <id>Apple Cake</id>
            <nr>1</nr>
            <hint>Sweet</hint>
            <userlevel>5</userlevel>                
        </item>
        <item>
            <id>Rainbow Cake</id>
            <nr>2</nr>
            <hint>Mine!!</hint>
            <userlevel>10</userlevel>               
        </item>
        <Category2WithApples>

        <CategoryMyOwn>
            <item>
                <id>Item in my Category</id>
                <nr>0</nr>
                <hint>Some text</hint>
                <userlevel>0</userlevel>                
            </item>
        </CategoryMyOwn>

    </menu>
</root>
Community
  • 1
  • 1
Kia
  • 301
  • 3
  • 11

1 Answers1

1

Your second XML is not a document, XML documents need to have a document element node. In other words here at the top level only a single element node is allowed. All other element nodes have to be descendants of that node.

You can treat this as an XML fragment however. A fragment is the inner XML of an element node.

In both cases it easier to use DOM for that.

Append a fragment to a parent element node

Let's keep it simple for the first step and append the fragment to the menu node.

$document = new DOMDocument();
$document->loadXml($targetXml);
$xpath = new DOMXpath($document);

$fragment = $document->createDocumentFragment();
$fragment->appendXml($fragmentXml);

foreach ($xpath->evaluate('/root/menu[1]') as $menu) {
  $menu->appendChild($fragment);
}

echo $document->saveXml();

The Xpath expression can /root/menu[1] selects the first menu element node inside the root. This can be only one node or none.

A document fragment in DOM is a node object and can be appended like any other node (element, text, ...).

Merging nodes

Merging the category nodes is a little more difficult. But Xpath will help.

$document = new DOMDocument();
$document->loadXml($targetXml);
$xpath = new DOMXpath($document);

$fragment = $document->createDocumentFragment();
$fragment->appendXml($fragmentXml);

$menu = $xpath->evaluate("/root/menu[1]")->item(0);

foreach ($xpath->evaluate('*', $fragment) as $category) {
  $targets = $xpath->evaluate("{$category->nodeName}[1]", $menu);
  if ($targets->length > 0) {
    $targetCategory = $targets->item(0);
    foreach ($category->childNodes as $item) {
      $targetCategory->appendChild($item);
    }
  } else {
    $menu->appendChild($category);
  }
}
echo $document->saveXml();

Fetching the menu node

$menu = $xpath->evaluate("/root/menu[1]")->item(0);

This is about the same like in the first simple example. It fetch the menu nodes in root and returns the first found node. You should check if the list contained a node. But for this example just take it for guaranteed.

Iterating the fragment

foreach ($xpath->evaluate('*', $fragment) as $category) {
  ...
}

* is a simple Xpath expression that returns any element child node. The fragment can contain other nodes (whitespace, text, comment, ...). The second argument for DOMXpath::evaluate() is the context for the Xpath expression.

Fetching the target category

Next you need to fetch the category node with the same name from the target document. This will return a list with one node or an empty list.

$targets = $xpath->evaluate("{$category->nodeName}[1]", $menu);
if ($targets->length > 0) {
  ...
} else {
  ...
}

Append to the found target category

If the category exists append all child nodes from the category in the fragment to the target.

$targetCategory = $targets->item(0);
foreach ($category->childNodes as $item) {
  $targetCategory->appendChild($item);
}

Append a category

$menu->appendChild($category);

If the category doesn't exists, just append it to the menu.

ThW
  • 19,120
  • 3
  • 22
  • 44
  • Would it be easier to change the second XML to have the same "format" as the first one? Like adding the root and menu nodes. – Kia Sep 14 '15 at 11:12
  • 1
    No, just a different. You could still use Xpath to fetch nodes from the second XML document. But if you load it into a second document object, you would have to use DOMDocument::importNode() to import the nodes into the target document. Basically you would have two Xpath objects (one for each document), the Xpath for the source would change a little (because you change the structure) and you would have to use something like `$targetCategory->appendChild($document->importNode($item, true));` DOM methods are designed to do only a single little task and to be combined. – ThW Sep 14 '15 at 12:21
  • can you mention what's the `$targetXml` and `$fragmentXml`? – Mohit Rane Jan 24 '23 at 09:26