6bytes
6bytes

Reputation: 6132

Parsing XML with missing nodes

I have an XML file with orders.

<?xml version="1.0" encoding="UTF-8"?>
<orders>
<order order-no="1234567">
    <order-date>2012-09-15T12:28:45.000Z</order-date>
    <invoice-no>123</invoice-no>
    <customer>
        <customer-no>1234</customer-no>
        <customer-name>test</customer-name>
    </customer>
    <payments>
        <payment>
            <custom-method>
                <method-name>example</method-name>
            </custom-method>
            <amount>12334</amount>
        </payment>
    </payments>
</order>
<order>
   .......
</order>
</orders>

Many <order> nodes in one file. Everything is correct but then out of the blue I get an order without any payment information like this

<orders>
<order order-no="1234567">
    <order-date>2012-09-15T12:28:45.000Z</order-date>
    <invoice-no>123</invoice-no>
    <customer>
        <customer-no>1234</customer-no>
        <customer-name>test</customer-name>
        <billing-address/>
    </customer>
    <payments/>
</order>

Notice the empty <payments/> node. My PHP script looks like this:

$file = file_get_contents('orders.xml');
$orders = new SimpleXMLElement($file);
foreach ($orders as $order) {
    $var = $order->payments->payment->{'custom-method'}->{'method-name'};
}

With the first XML example everything is fine but when I stumble upon an order from a second XML example my script is throwing an exception Trying to get property of non-object and obviously stops parsing the rest of the file.
I'd like my script to not stop and just go on with parsing the rest of the file. I don't want to you the "shut up" @ operator, but I don't see any other way.

Upvotes: 0

Views: 740

Answers (2)

hakre
hakre

Reputation: 198214

SimpleXML comes with some bunch of magic. Let's see how you access the (existing or non-existing) <payment> element:

$order->payments->payment

This will (always!) result in a SimpleXMLElement regardless if the count of <payment> elements inside <payments> elements is zero or larger.

However, if the count is actually zero, accessing the first <payment> element will return NULL:

$order->payments->payment[0]

This is accessing the first (zero-based index in SimpleXML) payment element. As it does not exist, this results in NULL. So you can if-check this:

foreach ($orders as $order) 
{
    if (!$order->payments->payment[0]) 
        continue;

    $var = $order->payments->payment->{'custom-method'}->{'method-name'};
}

The other answer suggested to use the SimpleXMLElement::count() method. It counts the children of an element and it works as well, however the other answer suggested the wrong place to use it, the correct place is with the <payment> element(s), not the <payments> element:

$order->payments->payment->count()

This returns the 0 (zero) number of elememnts you wanted to check for. So this is working equally well:

foreach ($orders as $order) 
{
    if (!$order->payments->payment->count()) 
        continue;

    $var = $order->payments->payment->{'custom-method'}->{'method-name'};
}

However this excursion would not be complete to actually show you a way that is probably most intended with SimpleXMLElement. It is based on the fact that an empty SimpleXMLElement equals false:

foreach ($orders as $order) 
{
    if (!$order->payments->payment) 
        continue;

    $var = $order->payments->payment->{'custom-method'}->{'method-name'};
}

As all these examples show, you need explore SimpleXML a little to understand it's magic so you can benefit from it.

If you prefer an interface with less magic, checkout the sister DOMDocument.

And independent to SimpleXMLElement, whenever you've got a Traversable (which it is as well), you can make use of a function called iterator_count to give back the count of iterations. For a SimpleXMLElement it means the number of children.

Upvotes: 0

Jason McCreary
Jason McCreary

Reputation: 73031

You need to add logic to ensure <payments> has payment data (i.e. payment nodes).

foreach ($orders as $order) {
    if ($order->payments->count()) {
        // has payment data... run your payment code
    }
}

UPDATE

Even though the docs claim the above should work with PHP > 5.3, it returned 1 in the OP's case (not sure why).

Ultimately the following is needed:

count($order->payments->children())

Upvotes: 1

Related Questions