I am consuming location and address data from a public NHS API, it is coming in in an XML format and I have transformed it to JSON for use within my webapp.
My code is working and producing the desired result, however I cannot help but cringe at the amount of nested foreach loops inside this code.
Is there a cleaner way to achieve the same result that will be more performant?
I have tried using simplexml_load_string and many variations and it refuses to output/parse the content of a nested element containing s:organisationSummary data.
Here is the PHP class
class XmlElement {
public $name;
public $attributes;
public $content;
public $children;
}
class GpService
{
protected $apiKey;
public function __construct()
{
$this->apiKey = env('NHS_API_KEY');
}
public function xmlToObject($xml) {
$parser = xml_parser_create();
xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 1);
xml_parse_into_struct($parser, $xml, $tags);
xml_parser_free($parser);
$elements = array(); // the currently filling [child] XmlElement array
$stack = array();
foreach ($tags as $tag) {
$index = count($elements);
if ($tag['type'] == "complete" || $tag['type'] == "open") {
$elements[$index] = new XmlElement;
$elements[$index]->name = $tag['tag'];
$elements[$index]->attributes = (isset($tag['attributes']))?$tag['attributes']:null;
$elements[$index]->content = (isset($tag['value']))?$tag['value']:null;
if ($tag['type'] == "open") { // push
$elements[$index]->children = array();
$stack[count($stack)] = &$elements;
$elements = &$elements[$index]->children;
}
}
if ($tag['type'] == "close") { // pop
$elements = &$stack[count($stack) - 1];
unset($stack[count($stack) - 1]);
}
}
return $elements[0]; // the single top-level element
}
public function searchByPostcode($postcode)
{
$postcodeRequest = new \GuzzleHttp\Client();
$postcodeGet = $postcodeRequest->request('GET', 'https://api.nhs.uk/data/gppractices/postcode/' . $postcode . '/?distance=15', [
'headers' => [
'subscription-key' => $this->apiKey,
]
]);
//Convert the XML string into an SimpleXMLElement object.
$xmlObject = $this->xmlToObject($postcodeGet->getBody());
$outputObject = [];
// Absolutely insane loops to get data into the format I would like
foreach($xmlObject->children as $key => $entry)
{
if($entry->name === 'entry')
{
foreach($entry->children as $value)
{
if($value->name === 'content')
{
foreach($value->children as $objectProperty)
{
foreach($objectProperty->children as $dataItem)
{
if(is_array($dataItem->children))
{
foreach($dataItem->children as $childKey => $childDataItem)
{
if($childDataItem->name == 's:addressLine')
{
$childDataItem->name = $childDataItem->name . $childKey;
}
$outputObject[$key][str_replace('s:','',strtolower($childDataItem->name))] = $childDataItem->content;
}
}
else
{
$outputObject[$key][str_replace('s:','',strtolower($dataItem->name))] = $dataItem->content;
}
}
}
}
}
}
}
return $outputObject;
}
}
Here is the XML Data:
<feed xmlns:s="https://api.nhs.uk/data/services" xmlns="https://www.w3.org/2005/Atom">
<title type="text">NHS website - GP Practices Near Postcode - LE2 1DA - Within 15km</title>
<id>https://api.nhs.uk/data/gppractices/postcode/le21da?range=15</id>
<rights type="text">© Crown Copyright 2009</rights>
<updated>2019-08-22T15:04:20+01:00</updated>
<category term="Search"/>
<logo>https://www.nhs.uk/nhscwebservices/documents/logo1.jpg</logo>
<author>
<name>NHS website</name>
<uri>https://www.nhs.uk</uri>
<email>webservices#nhschoices.nhs.uk</email>
</author>
<link rel="self" type="application/xml" title="NHS website - GP Practices Near Postcode - LE2 1DA - Within 15km" href="https://api.nhs.uk/data/gppractices/postcode/LE21DA&range=15"/>
<link rel="first" type="application/xml" title="first" length="1000" href="https://api.nhs.uk/data/gppractices/postcode/LE21DA&range=15&page=1"/>
<link rel="next" type="application/xml" title="next" length="1000" href="https://api.nhs.uk/data/gppractices/postcode/LE21DA&range=15&page=2"/>
<link rel="last" type="application/xml" title="last" length="1000" href="https://api.nhs.uk/data/gppractices/postcode/LE21DA&range=15&page=10"/>
<tracking xmlns="https://api.nhs.uk/data/services"><img style="border: 0; width: 1px; height: 1px;" alt="" src="https://statse.webtrendslive.com/dcs2221tai1ckz5huxw0mfq86_1m2w/njs.gif?dcsuri=/organisations%2fgppractices%2fpostcode%2fLE21DA&wt.cg_s=OrganisationData&wt.cg_n=syndication&synduserid=7a6ea2a0-8dec-11e9-8e02-f5191ebd3281&syndreviewdate=22-08-2019T14:04&synduserid=7a6ea2a0-8dec-11e9-8e02-f5191ebd3281&syndreviewdate=22-08-2019T14:04"/></tracking>
<s:SearchCoords>52.6299,-1.11502</s:SearchCoords>
<entry>
<id>https://api.nhs.uk/data/gppractices/4863</id>
<title type="text">Highfields Surgery (R Wadhwa)</title>
<updated>2019-08-22T14:04:20Z</updated>
<link rel="self" title="Highfields Surgery (R Wadhwa)" href="https://api.nhs.uk/data/gppractices/4863"/>
<link rel="alternate" title="Highfields Surgery (R Wadhwa)" href="https://www.nhs.uk/Services/GP/Overview/DefaultView.aspx?id=42231"/>
<content type="application/xml">
<s:organisationSummary>
<s:name>Highfields Surgery (R Wadhwa)</s:name>
<s:odsCode>C82116</s:odsCode>
<s:address>
<s:addressLine>25 Severn Street</s:addressLine>
<s:addressLine>Leicester</s:addressLine>
<s:addressLine>Leicestershire</s:addressLine>
<s:postcode>LE2 0NN</s:postcode>
</s:address>
<s:contact type="General">
<s:telephone>01162543253</s:telephone>
</s:contact>
<s:geographicCoordinates>
<s:longitude>-1.11859357357025</s:longitude>
<s:latitude>52.6293983459473</s:latitude>
</s:geographicCoordinates>
<s:Distance>0.247038430918239</s:Distance>
</s:organisationSummary>
</content>
</entry>
<entry>
<id>https://api.nhs.uk/data/gppractices/4901</id>
<title type="text">Dr R Kapur & Partners</title>
<updated>2019-08-22T14:04:20Z</updated>
<link rel="self" title="Dr R Kapur & Partners" href="https://api.nhs.uk/data/gppractices/4901"/>
<link rel="alternate" title="Dr R Kapur & Partners" href="https://www.nhs.uk/Services/GP/Overview/DefaultView.aspx?id=36753"/>
<content type="application/xml">
<s:organisationSummary>
<s:name>Dr R Kapur & Partners</s:name>
<s:odsCode>C82659</s:odsCode>
<s:address>
<s:addressLine>St Peters Health Centre</s:addressLine>
<s:addressLine>Sparkenhoe Street</s:addressLine>
<s:addressLine>Leicester</s:addressLine>
<s:addressLine>Leicestershire</s:addressLine>
<s:postcode>LE2 0TA</s:postcode>
</s:address>
<s:contact type="General">
<s:telephone>01162951258</s:telephone>
</s:contact>
<s:geographicCoordinates>
<s:longitude>-1.11907768249512</s:longitude>
<s:latitude>52.6310386657715</s:latitude>
</s:geographicCoordinates>
<s:Distance>0.30219913005026</s:Distance>
</s:organisationSummary>
</content>
</entry>
<entry>
<id>https://api.nhs.uk/data/gppractices/4827</id>
<title type="text">Shefa Medical Practice</title>
<updated>2019-08-22T14:04:20Z</updated>
<link rel="self" title="Shefa Medical Practice" href="https://api.nhs.uk/data/gppractices/4827"/>
<link rel="alternate" title="Shefa Medical Practice" href="https://www.nhs.uk/Services/GP/Overview/DefaultView.aspx?id=38960"/>
<content type="application/xml">
<s:organisationSummary>
<s:name>Shefa Medical Practice</s:name>
<s:odsCode>C82080</s:odsCode>
<s:address>
<s:addressLine>St Peters Health Centre</s:addressLine>
<s:addressLine>Sparkenhoe Street</s:addressLine>
<s:addressLine>Leicester</s:addressLine>
<s:addressLine>Leicestershire</s:addressLine>
<s:postcode>LE2 0TA</s:postcode>
</s:address>
<s:contact type="General">
<s:telephone>01162957835</s:telephone>
</s:contact>
<s:geographicCoordinates>
<s:longitude>-1.11907768249512</s:longitude>
<s:latitude>52.6310386657715</s:latitude>
</s:geographicCoordinates>
<s:Distance>0.30219913005026</s:Distance>
</s:organisationSummary>
</content>
</entry>
<entry>
<id>https://api.nhs.uk/data/gppractices/4902</id>
<title type="text">St Peter's Health Centre - Mansingh & Partner</title>
<updated>2019-08-22T14:04:20Z</updated>
<link rel="self" title="St Peter's Health Centre - Mansingh & Partner" href="https://api.nhs.uk/data/gppractices/4902"/>
<link rel="alternate" title="St Peter's Health Centre - Mansingh & Partner" href="https://www.nhs.uk/Services/GP/Overview/DefaultView.aspx?id=35950"/>
<content type="application/xml">
<s:organisationSummary>
<s:name>St Peter's Health Centre - Mansingh & Partner</s:name>
<s:odsCode>C82660</s:odsCode>
<s:address>
<s:addressLine>Sparkenhoe Street</s:addressLine>
<s:addressLine>Leicester</s:addressLine>
<s:addressLine>Leicestershire</s:addressLine>
<s:postcode>LE2 0TA</s:postcode>
</s:address>
<s:contact type="General">
<s:telephone>01162957827</s:telephone>
</s:contact>
<s:geographicCoordinates>
<s:longitude>-1.11907768249512</s:longitude>
<s:latitude>52.6310386657715</s:latitude>
</s:geographicCoordinates>
<s:Distance>0.30219913005026</s:Distance>
</s:organisationSummary>
</content>
</entry>
<entry>
<id>https://api.nhs.uk/data/gppractices/4848</id>
<title type="text">Al-Waqas Medical Centre</title>
<updated>2019-08-22T14:04:20Z</updated>
<link rel="self" title="Al-Waqas Medical Centre" href="https://api.nhs.uk/data/gppractices/4848"/>
<link rel="alternate" title="Al-Waqas Medical Centre" href="https://www.nhs.uk/Services/GP/Overview/DefaultView.aspx?id=43636"/>
<content type="application/xml">
<s:organisationSummary>
<s:name>Al-Waqas Medical Centre</s:name>
<s:odsCode>C82099</s:odsCode>
<s:address>
<s:addressLine>AL-Waqas Medical Practice</s:addressLine>
<s:addressLine>91 St Peters Road</s:addressLine>
<s:addressLine>Leicester</s:addressLine>
<s:addressLine>Leicestershire</s:addressLine>
<s:postcode>LE2 1DJ</s:postcode>
</s:address>
<s:contact type="General">
<s:telephone>01162543003</s:telephone>
</s:contact>
<s:geographicCoordinates>
<s:longitude>-1.10840618610382</s:longitude>
<s:latitude>52.6290435791016</s:latitude>
</s:geographicCoordinates>
<s:Distance>0.458084198394662</s:Distance>
</s:organisationSummary>
</content>
</entry>
<entry>
<id>https://api.nhs.uk/data/gppractices/4889</id>
<title type="text">Community Health Centre</title>
<updated>2019-08-22T14:04:20Z</updated>
<link rel="self" title="Community Health Centre" href="https://api.nhs.uk/data/gppractices/4889"/>
<link rel="alternate" title="Community Health Centre" href="https://www.nhs.uk/Services/GP/Overview/DefaultView.aspx?id=42802"/>
<content type="application/xml">
<s:organisationSummary>
<s:name>Community Health Centre</s:name>
<s:odsCode>C82643</s:odsCode>
<s:address>
<s:addressLine>Melbourne Centre</s:addressLine>
<s:addressLine>Melbourne Road</s:addressLine>
<s:addressLine>Leicester</s:addressLine>
<s:addressLine>Leicestershire</s:addressLine>
<s:postcode>LE2 0GU</s:postcode>
</s:address>
<s:contact type="General">
<s:telephone>01162622946</s:telephone>
</s:contact>
<s:geographicCoordinates>
<s:longitude>-1.11336529254913</s:longitude>
<s:latitude>52.6338653564453</s:latitude>
</s:geographicCoordinates>
<s:Distance>0.462480666213577</s:Distance>
</s:organisationSummary>
</content>
</entry>
<entry>
<id>https://api.nhs.uk/data/gppractices/4801</id>
<title type="text">Sayeed Medical Centre</title>
<updated>2019-08-22T14:04:20Z</updated>
<link rel="self" title="Sayeed Medical Centre" href="https://api.nhs.uk/data/gppractices/4801"/>
<link rel="alternate" title="Sayeed Medical Centre" href="https://www.nhs.uk/Services/GP/Overview/DefaultView.aspx?id=42819"/>
<content type="application/xml">
<s:organisationSummary>
<s:name>Sayeed Medical Centre</s:name>
<s:odsCode>C82060</s:odsCode>
<s:address>
<s:addressLine>352-354 East Park Road</s:addressLine>
<s:addressLine>Leicester</s:addressLine>
<s:addressLine>Leicestershire</s:addressLine>
<s:postcode>LE5 5AY</s:postcode>
</s:address>
<s:contact type="General">
<s:telephone>01163232030</s:telephone>
</s:contact>
<s:geographicCoordinates>
<s:longitude>-1.10658276081085</s:longitude>
<s:latitude>52.630802154541</s:latitude>
</s:geographicCoordinates>
<s:Distance>0.581034496166074</s:Distance>
</s:organisationSummary>
</content>
</entry>
<entry>
<id>https://api.nhs.uk/data/gppractices/4759</id>
<title type="text">Melbourne Street Surgery</title>
<updated>2019-08-22T14:04:20Z</updated>
<link rel="self" title="Melbourne Street Surgery" href="https://api.nhs.uk/data/gppractices/4759"/>
<link rel="alternate" title="Melbourne Street Surgery" href="https://www.nhs.uk/Services/GP/Overview/DefaultView.aspx?id=36620"/>
<content type="application/xml">
<s:organisationSummary>
<s:name>Melbourne Street Surgery</s:name>
<s:odsCode>C82031</s:odsCode>
<s:address>
<s:addressLine>56 Melbourne Street</s:addressLine>
<s:addressLine>Leicester</s:addressLine>
<s:addressLine>Leicestershire</s:addressLine>
<s:postcode>LE2 0AS</s:postcode>
</s:address>
<s:contact type="General">
<s:telephone>01162536299</s:telephone>
</s:contact>
<s:geographicCoordinates>
<s:longitude>-1.11437559127808</s:longitude>
<s:latitude>52.636344909668</s:latitude>
</s:geographicCoordinates>
<s:Distance>0.717122615984056</s:Distance>
</s:organisationSummary>
</content>
</entry>
<entry>
<id>https://api.nhs.uk/data/gppractices/4770</id>
<title type="text">East Park Medical Centre - R P Pandya</title>
<updated>2019-08-22T14:04:20Z</updated>
<link rel="self" title="East Park Medical Centre - R P Pandya" href="https://api.nhs.uk/data/gppractices/4770"/>
<link rel="alternate" title="East Park Medical Centre - R P Pandya" href="https://www.nhs.uk/Services/GP/Overview/DefaultView.aspx?id=40574"/>
<content type="application/xml">
<s:organisationSummary>
<s:name>East Park Medical Centre - R P Pandya</s:name>
<s:odsCode>C82037</s:odsCode>
<s:address>
<s:addressLine>264-266 East Park Road</s:addressLine>
<s:addressLine>Leicester</s:addressLine>
<s:addressLine>Leicestershire</s:addressLine>
<s:postcode>LE5 5FD</s:postcode>
</s:address>
<s:contact type="General">
<s:telephone>01162736330</s:telephone>
</s:contact>
<s:geographicCoordinates>
<s:longitude>-1.10444343090057</s:longitude>
<s:latitude>52.633544921875</s:latitude>
</s:geographicCoordinates>
<s:Distance>0.822717143851754</s:Distance>
</s:organisationSummary>
</content>
</entry>
<entry>
<id>https://api.nhs.uk/data/gppractices/4834</id>
<title type="text">Evington Medical Centre</title>
<updated>2019-08-22T14:04:20Z</updated>
<link rel="self" title="Evington Medical Centre" href="https://api.nhs.uk/data/gppractices/4834"/>
<link rel="alternate" title="Evington Medical Centre" href="https://www.nhs.uk/Services/GP/Overview/DefaultView.aspx?id=36363"/>
<content type="application/xml">
<s:organisationSummary>
<s:name>Evington Medical Centre</s:name>
<s:odsCode>C82088</s:odsCode>
<s:address>
<s:addressLine>2 - 6 Halsbury Street</s:addressLine>
<s:addressLine>Leicester</s:addressLine>
<s:addressLine>Leicestershire</s:addressLine>
<s:postcode>LE2 1QA</s:postcode>
</s:address>
<s:contact type="General">
<s:telephone>0116 3190343</s:telephone>
</s:contact>
<s:geographicCoordinates>
<s:longitude>-1.10676002502441</s:longitude>
<s:latitude>52.624267578125</s:latitude>
</s:geographicCoordinates>
<s:Distance>0.834829502560211</s:Distance>
</s:organisationSummary>
</content>
</entry>
</feed>
This is the output I am expecting:
{
"success":true,
"data":{
"13":{
"name":"Highfields Surgery (R Wadhwa)",
"odscode":"C82116",
"addressline0":"25 Severn Street",
"addressline1":"Leicester",
"addressline2":"Leicestershire",
"postcode":"LE2 0NN",
"telephone":"01162543253",
"longitude":"-1.11859357357025",
"latitude":"52.6293983459473",
"distance":"0.247038430918239"
},
"14":{
"name":"Dr R Kapur & Partners",
"odscode":"C82659",
"addressline0":"St Peters Health Centre",
"addressline1":"Sparkenhoe Street",
"addressline2":"Leicester",
"addressline3":"Leicestershire",
"postcode":"LE2 0TA",
"telephone":"01162951258",
"longitude":"-1.11907768249512",
"latitude":"52.6310386657715",
"distance":"0.30219913005026"
},
"15":{
"name":"Shefa Medical Practice",
"odscode":"C82080",
"addressline0":"St Peters Health Centre",
"addressline1":"Sparkenhoe Street",
"addressline2":"Leicester",
"addressline3":"Leicestershire",
"postcode":"LE2 0TA",
"telephone":"01162957835",
"longitude":"-1.11907768249512",
"latitude":"52.6310386657715",
"distance":"0.30219913005026"
},
"16":{
"name":"St Peter's Health Centre - Mansingh & Partner",
"odscode":"C82660",
"addressline0":"Sparkenhoe Street",
"addressline1":"Leicester",
"addressline2":"Leicestershire",
"postcode":"LE2 0TA",
"telephone":"01162957827",
"longitude":"-1.11907768249512",
"latitude":"52.6310386657715",
"distance":"0.30219913005026"
},
"17":{
"name":"Al-Waqas Medical Centre",
"odscode":"C82099",
"addressline0":"AL-Waqas Medical Practice",
"addressline1":"91 St Peters Road",
"addressline2":"Leicester",
"addressline3":"Leicestershire",
"postcode":"LE2 1DJ",
"telephone":"01162543003",
"longitude":"-1.10840618610382",
"latitude":"52.6290435791016",
"distance":"0.458084198394662"
},
"18":{
"name":"Community Health Centre",
"odscode":"C82643",
"addressline0":"Melbourne Centre",
"addressline1":"Melbourne Road",
"addressline2":"Leicester",
"addressline3":"Leicestershire",
"postcode":"LE2 0GU",
"telephone":"01162622946",
"longitude":"-1.11336529254913",
"latitude":"52.6338653564453",
"distance":"0.462480666213577"
},
"19":{
"name":"Sayeed Medical Centre",
"odscode":"C82060",
"addressline0":"352-354 East Park Road",
"addressline1":"Leicester",
"addressline2":"Leicestershire",
"postcode":"LE5 5AY",
"telephone":"01163232030",
"longitude":"-1.10658276081085",
"latitude":"52.630802154541",
"distance":"0.581034496166074"
},
"20":{
"name":"Melbourne Street Surgery",
"odscode":"C82031",
"addressline0":"56 Melbourne Street",
"addressline1":"Leicester",
"addressline2":"Leicestershire",
"postcode":"LE2 0AS",
"telephone":"01162536299",
"longitude":"-1.11437559127808",
"latitude":"52.636344909668",
"distance":"0.717122615984056"
},
"21":{
"name":"East Park Medical Centre - R P Pandya",
"odscode":"C82037",
"addressline0":"264-266 East Park Road",
"addressline1":"Leicester",
"addressline2":"Leicestershire",
"postcode":"LE5 5FD",
"telephone":"01162736330",
"longitude":"-1.10444343090057",
"latitude":"52.633544921875",
"distance":"0.822717143851754"
},
"22":{
"name":"Evington Medical Centre",
"odscode":"C82088",
"addressline0":"2 - 6 Halsbury Street",
"addressline1":"Leicester",
"addressline2":"Leicestershire",
"postcode":"LE2 1QA",
"telephone":"0116 3190343",
"longitude":"-1.10676002502441",
"latitude":"52.624267578125",
"distance":"0.834829502560211"
}
}
}
You can use DOM and XPath expressions. All the data is inside the {https://api.nhs.uk/data/services}organisationSummary nodes.
The following example register the prefix service for the namespace https://api.nhs.uk/data/services (That is what the s in your example resolves to). Then it fetches any {https://api.nhs.uk/data/services}organisationSummary node using the //service:organisationSummary expression.
For each each organisation it builds up an $item variable using expressions for scalar values. A loop is used for the {https://api.nhs.uk/data/services}addressLine nodes.
// create the DOM document and load the XML
$document = new DOMDocument();
$document->loadXML($xml);
// create an Xpath instance for the document - register namespace
$xpath = new DOMXpath($document);
$xpath->registerNamespace('service', 'https://api.nhs.uk/data/services');
$json = [
"success" => true,
// force object for data - it will be an array otherwise
"data" => new stdClass()
];
// iterate over all organisationSummary nodes
foreach ($xpath->evaluate('//service:organisationSummary') as $index => $organisation) {
// fetch single values
$item = [
"name" => $xpath->evaluate('string(service:name)', $organisation),
"odscode" => $xpath->evaluate('string(service:odsCode)', $organisation),
"postcode" => $xpath->evaluate('string(service:address/service:postcode)', $organisation),
"telephone" => $xpath->evaluate('string(service:contact/service:telephone)', $organisation),
"longitude" => $xpath->evaluate('string(service:geographicCoordinates/service:longitude)', $organisation),
"latitude" => $xpath->evaluate('string(service:geographicCoordinates/service:latitude)', $organisation),
"distance" => $xpath->evaluate('string(service:Distance)', $organisation)
];
// add the addressLine values
foreach ($xpath->evaluate('service:address/service:addressLine', $organisation) as $lineIndex => $line) {
$item['addressline'.$lineIndex] = $line->textContent;
}
// add the $item
$json['data']->{$index} = $item;
}
echo json_encode($json, JSON_PRETTY_PRINT);
Output:
{
"success": true,
"data": {
"0": {
"name": "Highfields Surgery (R Wadhwa)",
"odscode": "C82116",
"postcode": "LE2 0NN",
"telephone": "01162543253",
"longitude": "-1.11859357357025",
"latitude": "52.6293983459473",
"distance": "0.247038430918239",
"addressline0": "25 Severn Street",
"addressline1": "Leicester",
"addressline2": "Leicestershire"
},
"1": {
"name": "Dr R Kapur & Partners",
"odscode": "C82659",
"postcode": "LE2 0TA",
"telephone": "01162951258",
"longitude": "-1.11907768249512",
"latitude": "52.6310386657715",
"distance": "0.30219913005026",
"addressline0": "St Peters Health Centre",
"addressline1": "Sparkenhoe Street",
"addressline2": "Leicester",
"addressline3": "Leicestershire"
},
...
You need to use proper methods to traverse the XML document, rather than looking at every single element and checking its name.
Your two best options are DOM methods and XPath. The former will be familiar to you if you've worked with front-end JavaScript code (e.g. document.getElementsByTagName or document.getElementById.) The latter has a steeper learning curve but is far more powerful. (XSLT is also a good option for transforming XML, but I'm not very familiar with it.)
Step one though, is to use the proper XML library. The XML functions you're using are very low level. I'd recommend using PHP's DomDocument extension instead. Let's dive in!
<?php
$xml = file_get_contents("nhs.xml");
// assume XML document is stored in a variable called $xml
$dom = new DomDocument;
$dom->loadXml($xml);
// thankfully we can ignore namespaces!
$summaries = $dom->getElementsByTagName("organisationSummary");
// go through each <organisationSummary> element
foreach ($summaries as $os) {
$entry_data = [
// note we're using $os and not $dom now, so we get child elements of this particular element
"name" => $os->getElementsByTagName("name")[0]->textContent,
"odscode" => $os->getElementsByTagName("odsCode")[0]->textContent,
"addressline0" => $os->getElementsByTagName("addressLine")[0]->textContent,
// provide a default empty string in case there's no further lines
"addressline1" => $os->getElementsByTagName("addressLine")[1]->textContent ?? "",
"addressline2" => $os->getElementsByTagName("addressLine")[2]->textContent ?? "",
"addressline3" => $os->getElementsByTagName("addressLine")[3]->textContent ?? "",
"postcode" => $os->getElementsByTagName("postcode")[0]->textContent,
"telephone" => $os->getElementsByTagName("telephone")[0]->textContent,
"longitude" => $os->getElementsByTagName("longitude")[0]->textContent,
"latitude" => $os->getElementsByTagName("latitude")[0]->textContent,
"distance" => $os->getElementsByTagName("Distance")[0]->textContent,
];
$json_data[] = $entry_data;
}
if (count($json_data)) {
$output = ["success" => true, "data" => $json_data];
} else {
$output = ["success" => false];
}
echo json_encode($output, JSON_PRETTY_PRINT);
This is part of XML document:
<entry>
<author>
<name>Dunnock_D</name>
<uri>http://www.flickr.com/people/dunnock_d/</uri>
</author>
<link rel="license" type="text/html" href="https://creativecommons.org/licenses/by-nc/2.0/deed.en" />
<link rel="enclosure" type="image/jpeg" href="http://farm8.staticflickr.com/7548/26820724620_1d221c3187_b.jpg" />
</entry>
My code:
$xml = simplexml_load_string($result);
foreach ($xml->entry as $pixinfo) {
echo $pixinfo->link[1]['href'];
}
The problem is there can be one or more link strings and I need only particular with rel="enclosure" attribute.
What is the easiest way without extra IF and loops?
Thank you!
For that you can use DOMXPath, more specifically the query function. Let's say your $result variable contains the following:
<?xml version='1.0' encoding='UTF-8'?>
<entries>
<entry>
<author>
<name>Dunnock_D</name>
<uri>http://www.flickr.com/people/dunnock_d/</uri>
</author>
<link rel="license" type="text/html" href="https://creativecommons.org/licenses/by-nc/2.0/deed.en" />
<link rel="enclosure" type="image/jpeg" href="http://farm8.staticflickr.com/7548/26820724620_1d221c3187_b.jpg" />
</entry>
<entry>
<author>
<name>Dunnock_D</name>
<uri>http://www.flickr.com/people/dunnock_d/</uri>
</author>
<link rel="license" type="text/html" href="https://creativecommons.org/licenses/by-nc/2.0/deed.en" />
<link rel="enclosure" type="image/jpeg" href="http://farm8.staticflickr.com/7548/26820724620_1d221c3187_b.jpg" />
</entry>
<entry>
<author>
<name>Dunnock_D</name>
<uri>http://www.flickr.com/people/dunnock_d/</uri>
</author>
<link rel="license" type="text/html" href="https://creativecommons.org/licenses/by-nc/2.0/deed.en" />
<link rel="enclosure" type="image/jpeg" href="http://farm8.staticflickr.com/7548/26820724620_1d221c3187_b.jpg" />
</entry>
</entries>
I know the entries are repeated, but it's only for demo purposes. The code to get only the enclosure links would be:
$doc = new DOMDocument();
$doc->preserveWhiteSpace = false;
$doc->loadXML($result);
$xpath = new DOMXpath($doc);
$entries = $xpath->query('//entries/entry');
foreach ($entries as $entry) {
$link = $xpath->query('link[#rel="enclosure"]', $entry)->item(0);
$href = $link->getAttribute('href');
echo "{$href}\n";
}
You are using simplexml. Just use "attributes()" function: http://php.net/manual/pt_BR/simplexmlelement.attributes.php
Or you can access directly:
foreach ($xml->entry as $pixinfo) {
if($pixinfo->link[1]['rel'] == 'enclosure') {
echo $pixinfo->link[1]['href'];
}
}
The solution is Xpath.
With SimpleXML you can fetch the attribute node and cast the generated SimpleXMLElement into a string. You should make sure that you got an element before you cast it. SimpleXMLElement::xpath() will always return an array of SimpleXMLElement objects.
$entry = new SimpleXMLElement($xml);
$enclosures = $entry->xpath('link[#rel="enclosure"]/#href');
if (count($enclosures) > 0) {
var_dump((string)$enclosures[0]);
}
Output:
string(63) "http://farm8.staticflickr.com/7548/26820724620_1d221c3187_b.jpg"
With DOM the bootstrap is slightly larger, but you can fetch the href attribute directly as a string:
$document = new DOMDocument();
$document->loadXml($xml);
$xpath = new DOMXpath($document);
var_dump(
$xpath->evaluate('string(/entry/link[#rel="enclosure"]/#href)')
);
This will return an empty string if the expression does not match.
I have looked through several posts on how to parse the namespace of a Google Analytics api feed (XML file) in PHP. They all say I need to have this bit of code:
$properties = $item->children('http://schemas.google.com/analytics/2009');
(or something similar, all involving the URL "http://schemas.google.com/analytics/2009")
The problem is, however, that I then get an error:
Fatal error: Call to a member function children() on a non-object
It seems as though the schemas.google.com doesn't exist, but I can't find where I need to be pointing to... any ideas?
Here's a snippet of the XML file I'm working with:
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:dxp="http://schemas.google.com/analytics/2009" xmlns:openSearch="http://a9.com/-/spec/opensearch/1.1/">
<id>https://www.googleapis.com/analytics/v2.4/data?ids=ga:42418300&dimensions=ga:pagePath&metrics=ga:pageviews,ga:uniquePageviews&sort=-ga:pageviews&start-date=2013-12-01&end-date=2014-01-01&max-results=10</id>
<updated>2014-01-03T22:21:31.057Z</updated>
<title type="text">Google Analytics Data for View (Profile) xxxxxxx</title>
<link rel="self" type="application/atom+xml" href="https://www.googleapis.com/analytics/v2.4/data?ids=ga:42418300&dimensions=ga:pagePath&metrics=ga:pageviews,ga:uniquePageviews&sort=-ga:pageviews&start-date=2013-12-01&end-date=2014-01-01&max-results=10"/>
<link rel="next" type="application/atom+xml" href="https://www.googleapis.com/analytics/v2.4/data?ids=ga:42418300&dimensions=ga:pagePath&metrics=ga:pageviews,ga:uniquePageviews&sort=-ga:pageviews&start-date=2013-12-01&end-date=2014-01-01&start-index=11&max-results=10"/>
<author>
<name>Google Analytics</name>
</author>
<generator>Google Analytics</generator>
<openSearch:totalResults>4826</openSearch:totalResults>
<openSearch:startIndex>1</openSearch:startIndex>
<openSearch:itemsPerPage>10</openSearch:itemsPerPage>
<dxp:aggregates>
<dxp:metric name="ga:pageviews" type="integer" value="166656"/>
<dxp:metric name="ga:uniquePageviews" type="integer" value="132895"/>
</dxp:aggregates>
<dxp:containsSampledData>false</dxp:containsSampledData>
<dxp:dataSource>
<dxp:property name="ga:profileId" value="xxxxxxxx"/>
<dxp:property name="ga:webPropertyId" value="UA-xxxxxxxx-1"/>
<dxp:property name="ga:accountName" value="Provost"/>
<dxp:tableId>ga:42418300</dxp:tableId>
<dxp:tableName>WebSite</dxp:tableName>
</dxp:dataSource>
<dxp:endDate>2014-01-01</dxp:endDate>
<dxp:startDate>2013-12-01</dxp:startDate>
<entry>
<id>https://www.googleapis.com/analytics/v2.4/data?ids=ga:42418300&ga:pagePath=/&start-date=2013-12-01&end-date=2014-01-01</id>
<updated>2014-01-03T22:21:31.057Z</updated>
<title type="text">ga:pagePath=/</title>
<link rel="alternate" type="text/html" href="http://www.google.com/analytics"/>
<dxp:dimension name="ga:pagePath" value="/"/>
<dxp:metric name="ga:pageviews" type="integer" value="38197"/>
<dxp:metric name="ga:uniquePageviews" type="integer" value="29385"/>
</entry>
<entry>
<id>https://www.googleapis.com/analytics/v2.4/data?ids=ga:42418300&ga:pagePath=/page2/&start-date=2013-12-01&end-date=2014-01-01</id>
<updated>2014-01-03T22:21:31.057Z</updated>
<title type="text">ga:pagePath=/page2/</title>
<link rel="alternate" type="text/html" href="http://www.google.com/analytics"/>
<dxp:dimension name="ga:pagePath" value="/page2/"/>
<dxp:metric name="ga:pageviews" type="integer" value="13964"/>
<dxp:metric name="ga:uniquePageviews" type="integer" value="10974"/>
</entry>
Here's my PHP so far:
<?php
//ini_set('auto_detect_line_endings',TRUE);
$xml = simplexml_load_file("/ga-feed.xml");
$namespaces = $xml->getNamespaces(true);
foreach ($xml->entry as $key => $value) {
$value->registerXPathNamespace('dxp', 'http://schemas.google.com/analytics/2009');
echo $value->xpath('dxp:metric[pageviews]') . "<br />\n";
}
?>
Eventually, I need to be able run some calculations with pageviews and unique pageviews (show top 4 sites, a 5th "Other" would be a combination of all the other sites) against the overall pageviews/unique pageviews. Am I at least going in a semi-correct direction?
Update: In my foreach, I removed "feed" so it just says $xml->entry as $key, and now it will display a list of text:
Array
Array
Array
Array
Array
Array
Array
Array
Array
Array
Not quite what I'm looking for... but progress? lol
I have the following XML code that I'm trying to parse, but I'm sure of how to traverse some of the data in PHP:
<entry>
<id>http://data.treasury.gov:8001/Feed.svc/DailyTreasuryYieldCurveRateData(5360)</id>
<title type="text"></title>
<updated>2011-06-09T20:15:18Z</updated>
<author>
<name />
</author>
<link rel="edit" title="DailyTreasuryYieldCurveRateDatum" href="DailyTreasuryYieldCurveRateData(5360)" />
<category term="TreasuryDataWarehouseModel.DailyTreasuryYieldCurveRateDatum" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
<content type="application/xml">
<m:properties>
<d:Id m:type="Edm.Int32">5360</d:Id>
<d:NEW_DATE m:type="Edm.DateTime">2011-06-01T00:00:00</d:NEW_DATE>
<d:BC_1MONTH m:type="Edm.Double">0.04</d:BC_1MONTH>
<d:BC_3MONTH m:type="Edm.Double">0.05</d:BC_3MONTH>
<d:BC_6MONTH m:type="Edm.Double">0.11</d:BC_6MONTH>
<d:BC_1YEAR m:type="Edm.Double">0.18</d:BC_1YEAR>
<d:BC_2YEAR m:type="Edm.Double">0.44</d:BC_2YEAR>
<d:BC_3YEAR m:type="Edm.Double">0.74</d:BC_3YEAR>
<d:BC_5YEAR m:type="Edm.Double">1.6</d:BC_5YEAR>
<d:BC_7YEAR m:type="Edm.Double">2.28</d:BC_7YEAR>
<d:BC_10YEAR m:type="Edm.Double">2.96</d:BC_10YEAR>
<d:BC_20YEAR m:type="Edm.Double">3.83</d:BC_20YEAR>
<d:BC_30YEAR m:type="Edm.Double">4.15</d:BC_30YEAR>
<d:BC_30YEARDISPLAY m:type="Edm.Double">4.15</d:BC_30YEARDISPLAY>
</m:properties>
</content>
</entry>
I can only get so far as
entry->content
As the following throws an error for having a colon:
entry->content->m:properties
How do I access what's inside content such as d:NEW_DATE?
In SimpleXML you can use the children('prefix', true) and attributes('prefix', true) functions to access namespaced content.
entry->content->children('m', true)->properties
or to access d:NEW_DATE
entry->content->children('m', true)->properties->children('d', true)->NEW_DATE
or one step further to access the m:type attribute
entry->content->children('m', true)->properties->children('d', true)->NEW_DATE->attributes('m', true)->type
You can use the SimpleXml's functions
SimpleXML
But my fav class is DOMDocument