'Struggling to get attributes from XML (PHP) [duplicate]

I have the following XML returned by an API:

<ns3:calculateResponse xmlns="http://geomodel.eu/schema/common/geo" xmlns:ns2="http://geomodel.eu/schema/common/pv" xmlns:ns3="http://geomodel.eu/schema/ws/pvplanner">
<ns3:site lat="48.61259" lng="20.827079">
<terrain elevation="246" tilt="10.0" azimuth="176"/>
<horizon/>
<ns2:geometry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="ns2:GeometryFixedOneAngle" tilt="10.0" azimuth="175"/>
<ns2:system installedPower="1.0" installationType="ROOF_MOUNTED" availability="99.0">
<ns2:module type="CSI"/>
<ns2:inverter>
<ns2:efficiency xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="ns2:EfficiencyConstant" percent="97.5"/>
</ns2:inverter>
<ns2:losses dc="5.5" ac="1.5"/>
</ns2:system>
</ns3:site>
<ns3:irradiation>
<ns3:reference>
<ns3:Ghm monthly="31.0 49.4 97.2 126.6 159.0 166.3 165.4 152.6 101.8 65.6 33.7 23.8" yearly="1172.4"/>
<ns3:Ghd monthly="1.00 1.76 3.14 4.22 5.13 5.54 5.34 4.92 3.39 2.12 1.12 0.77" yearly="3.21"/>
<ns3:Dhd monthly="0.57 0.91 1.47 2.06 2.50 2.75 2.62 2.27 1.67 1.11 0.67 0.46" yearly="1.59"/>
<ns3:Td monthly="-2.5 -1.1 3.0 8.5 13.5 17.1 19.8 19.5 13.7 8.6 3.1 -1.8" yearly="8.5"/>
<ns3:Tmin monthly="-3.9 -3.0 0.2 4.1 7.9 11.2 13.8 14.0 9.2 5.6 1.6 -2.8"/>
<ns3:Tmax monthly="0.2 2.1 6.9 13.7 19.1 22.6 25.5 25.7 19.4 13.2 6.0 0.6"/>
<ns3:invar monthly="-1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0" yearly="-1.0"/>
<ns3:Rh monthly="90.0 90.0 80.0 73.0 72.0 67.0 63.0 61.0 69.0 78.0 86.0 87.0" yearly="76.0"/>
<ns3:Pwat monthly="9.0 9.0 10.0 13.0 18.0 22.0 25.0 24.0 19.0 15.0 12.0 9.0" yearly="15.0"/>
</ns3:reference>
<ns3:inplane>
<ns3:Gim monthly="39.9 59.7 110.5 136.4 165.8 171.2 171.5 162.6 112.6 76.8 41.6 30.9" yearly="1279.5"/>
<ns3:Gid monthly="1.28 2.13 3.56 4.54 5.34 5.71 5.53 5.24 3.75 2.48 1.39 0.99" yearly="3.50"/>
<ns3:Did monthly="0.62 0.98 1.57 2.15 2.57 2.82 2.69 2.37 1.76 1.19 0.72 0.50" yearly="1.67"/>
<ns3:Rid monthly="0.00 0.00 0.00 0.00 0.00 0.01 0.01 0.00 0.00 0.00 0.00 0.00" yearly="0.00"/>
<ns3:ShLoss monthly="0.5 0.4 0.3 0.4 0.4 0.5 0.5 0.3 0.4 0.4 0.5 0.5" yearly="0.4"/>
</ns3:inplane>
<ns3:comparison>
<ns3:horizontal yearlySum="1172.0" percentOpt="84.2"/>
<ns3:optimum yearlySum="1393.0" percentOpt="100.0"/>
<ns3:tracker2x yearlySum="1751.0" percentOpt="125.7"/>
<ns3:selected yearlySum="1279.0" percentOpt="91.9"/>
</ns3:comparison>
<ns3:optimum fixed="37.0"/>
</ns3:irradiation>
<ns3:calculation>
<ns3:output>
<ns3:Esm monthly="33.0 50.6 93.1 111.2 131.7 133.6 132.0 125.6 89.6 62.4 33.8 24.9" yearly="1021.5"/>
<ns3:Esd monthly="1.06 1.81 3.00 3.71 4.25 4.45 4.26 4.05 2.99 2.01 1.13 0.80" yearly="2.80"/>
<ns3:Etm monthly="33.0 50.6 93.1 111.2 131.7 133.6 132.0 125.6 89.6 62.4 33.8 24.9" yearly="1021.5"/>
<ns3:Eshare monthly="3.2 5.0 9.1 10.9 12.9 13.1 12.9 12.3 8.8 6.1 3.3 2.4" yearly="100.0"/>
<ns3:PR monthly="82.4 84.4 84.0 81.2 79.1 77.6 76.6 77.0 79.3 80.9 80.9 80.3" yearly="79.5"/>
</ns3:output>
<ns3:losses>
<ns3:global output="1285" PRp="100.0" PRc="100.0"/>
<ns3:terrain output="1279" lossAbs="-5" lossRel="-0.42" PRp="99.6" PRc="99.6"/>
<ns3:angular output="1231" lossAbs="-49" lossRel="-3.81" PRp="96.2" PRc="95.8"/>
<ns3:conversion output="1137" lossAbs="-94" lossRel="-7.63" PRp="92.4" PRc="88.5"/>
<ns3:dcLoss output="1074" lossAbs="-63" lossRel="-5.5" PRp="94.5" PRc="83.6"/>
<ns3:inverter output="1047" lossAbs="-27" lossRel="-2.5" PRp="97.5" PRc="81.5"/>
<ns3:acLoss output="1032" lossAbs="-16" lossRel="-1.5" PRp="98.5" PRc="80.3"/>
<ns3:availability output="1021" lossAbs="-10" lossRel="-1.0" PRp="99.0" PRc="79.5"/>
<ns3:total output="1021" lossAbs="-264" lossRel="-20.51" PRc="79.5"/>
</ns3:losses>
</ns3:calculation>
<ns3:summary>PV system: 1.0 kWp, crystalline silicon, fixed roof, azim. 175&deg; (south), inclination 10&deg;</ns3:summary>
</ns3:calculateResponse>

and, in PHP, I need to obtain the:

ns3:calculateResponse -> ns3:irradiation -> ns3:inplane -> ns3:Gim

attributes: monthly and yearly

But I can't!

$xml = simplexml_load_string($response);
foreach($xml->calculateResponse[0]->attributes() as $a => $b) {
    echo $a,'="',$b,"\"\n";
}

returns

Uncaught Error: Call to a member function attributes()

and if I use ns3: anywhere in names, seemingly the colon breaks everything.

I have tried a number of methods from documented standard examples, but there is seemingly something with this particular XML format causing problems.

Either that, or it's my brain causing the problems!

Any advice, most appreciated.

Thanks



Solution 1:[1]

There is one problem with this XML sample: it contains a HTML entity &deg; that is not a valid XML entity. The contents of <ns3:summary> should probably be wrapped in a CDATA object.

If you fix that, you can register the ns3 namespace and use XPath to get to the <ns3:Gim> entities:

$xml = simplexml_load_string($response);
$xml->registerXPathNamespace('ns3', 'http://geomodel.eu/schema/ws/pvplanner');
foreach ($xml->xpath('//ns3:irradiation/ns3:inplane/ns3:Gim') as $gim) {
    foreach($gim->attributes() as $a => $b) {
        echo $a,'="',$b,"\"\n";
    }    
}

/*
monthly="39.9 59.7 110.5 136.4 165.8 171.2 171.5 162.6 112.6 76.8 41.6 30.9"
yearly="1279.5"
*/

https://3v4l.org/HLO40

Edit:

If you don't have control over the incoming XML, you can add a custom DTD to the beginning of $response that defines the HTML entities that can occur in the document and transforms them into valid (numeric) XML entities:

$dtd = '<!DOCTYPE calculateResponse [
  <!ENTITY deg "&#176;">
]>';

$xml = simplexml_load_string($dtd . $response);

If the XML can contain any possible HTML entity, you can consider adding the full list of HTML entities from the XHTML specification at https://www.w3.org/TR/xhtml-modularization/dtd_module_defs.html#a_dtd_xhtml_character_entities

Just keep in mind that the name inside the DOCTYPE declaration must be identical to the non-namespaced root element in your XML code. In this case, ns3:calculateResponse becomes calculateResponse.

Solution 2:[2]

Without control of the XML at source, I used the following to 'fix' the invalid &deg; entity:

function load_invalid_xml($xml)
{
    $use_internal_errors = libxml_use_internal_errors(true);
    libxml_clear_errors(true);

    $sxe = simplexml_load_string($xml);

    if ($sxe)
    {
        return $sxe;
    }

    $fixed_xml = '';
    $last_pos  = 0;

    $xml = str_replace("&deg;", "degrees", $xml);

    // get file encoding
    $encoding = mb_detect_encoding($xml);

    foreach (libxml_get_errors() as $error)
    {
        $pos = $error->column;
        $invalid_char = mb_substr($xml, $pos, 1, $encoding);
        $fixed_xml .= substr($xml, $last_pos, $pos - $last_pos) . htmlspecialchars($invalid_char);
        $last_pos = $pos + 1;
    }
    $fixed_xml .= substr($xml, $last_pos);

    libxml_use_internal_errors($use_internal_errors);

    return $fixed_xml;
}

So, then the full solution is:

$xml = load_invalid_xml($string);

$xml = simplexml_load_string($xml);
$xml->registerXPathNamespace('ns3', 'http://geomodel.eu/schema/ws/pvplanner');
foreach ($xml->xpath('//ns3:irradiation/ns3:inplane/ns3:Gim') as $gim) {
    foreach($gim->attributes() as $a => $b) {
        echo $a,'="',$b,"\"<br>";
    }    
}

Thanks for your help @rickdenhaan

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 rjbathgate