views:

585

answers:

1

I need some help on the SimpleXML calls for a recursive function that lists the elements name and attributes. Making a XML config file system but each script will have it's own config file as well as a new naming convention. So what I need is an easy way to map out all the elements that have attributes, so like in example 1 I need a simple way to call all the processes but I don't know how to do this without hard coding the elements name is the function call. Is there a way to recursively call a function to match a child element name? I did see the xpath functionality but I don't see how to use this for attributes. Any ideas?

Also does the XML in the examples look correct? can I structure my XML like this?

Example 1:

<application>
  <processes>
    <process id="123" name="run batch A" />
    <process id="122" name="run batch B" />
    <process id="129" name="run batch C" />
  </processes>
  <connections>
    <databases>
      <database usr="test" pss="test" hst="test" dbn="test" />
    </databases>
    <shells>
      <ssh usr="test" pss="test" hst="test-2" />
      <ssh usr="test" pss="test" hst="test-1" />
    </shells>
  </connections>
</application>

Example 2:

<config>
  <queues>
    <queue id="1" name="test" />
    <queue id="2" name="production" />
    <queue id="3" name="error" />
  </queues>
</config>

Pseudo code:

// Would return matching process id
getProcess($process_id) {
  return the process attributes as array that are in the XML
}

// Would return matching DBN (database name)
getDatabase($database_name) {
  return the database attributes as array that are in the XML
}

// Would return matching SSH Host
getSSHHost($ssh_host) {
  return the ssh attributes as array that are in the XML
}

// Would return matching SSH User
getSSHUser($ssh_user) {
  return the ssh attributes as array that are in the XML
}

// Would return matching Queue 
getQueue($queue_id) {
  return the queue attributes as array that are in the XML
}

EDIT:

Can I pass two parms? on the first method you have suggested @Gordon

I just got it, thnx, see below

public function findProcessById($id, $name)
{
    $attr = false;
    $el = $this->xml->xpath("//process[@id='$id'][@name='$name']"); // How do I also filter by the name?
    if($el && count($el) === 1) {
        $attr = (array) $el[0]->attributes();
        $attr = $attr['@attributes'];
    }
    return $attr;
}
+2  A: 

The XML looks good to me. The only thing I wouldn't do is making name an attribute in process, because it contains spaces and should be a textnode then (in my opinion). But since SimpleXml does not complain about it, I guess it boils down to personal preference.

I'd likely approach this with a DataFinder class, encapsulating XPath queries, e.g.

class XmlFinder
{
    protected $xml;
    public function __construct($xml)
    {
        $this->xml = new SimpleXMLElement($xml);
    }
    public function findProcessById($id)
    {
        $attr = false;
        $el = $this->xml->xpath("//process[@id='$id']");
        if($el && count($el) === 1) {
            $attr = (array) $el[0]->attributes();
            $attr = $attr['@attributes'];
        }
        return $attr;
    }
    // ... other methods ...
}

and then use it with

$finder = new XmlFinder($xml);
print_r( $finder->findProcessById(122) );

Output:

Array
(
    [id] => 122
    [name] => run batch B
)

XPath tutorial:


An alternative would be to use SimpleXmlIterator to iterate over the elements. Iterators can be decorated with other Iterators, so you can do:

class XmlFilterIterator extends FilterIterator
{
    protected $filterElement;
    public function setFilterElement($name)
    {
        $this->filterElement = $name;
    }
    public function accept()
    {
        return ($this->current()->getName() === $this->filterElement);
    }
}

$sxi = new XmlFilterIterator(
           new RecursiveIteratorIterator( 
               new SimpleXmlIterator($xml)));

$sxi->setFilterElement('process');

foreach($sxi as $el) {
    var_dump( $el ); // will only give process elements
}

You would have to add some more methods to have the filter work for attributes, but this is a rather trivial task.

Introduction to SplIterators:

Gordon
Thanks a ton, the first solution looks great but I have an error: PHP Fatal error: Uncaught exception 'Exception' with message 'String could not be parsed as XML'. Im passing the file like this. $xml = "/path/to/file.xml"; is this correct?
Phill Pafford
@Phill No, it expects the XML string, not the filepath. If you want to pass a filepath, you have to pass `$data_is_url` as third param. See http://de3.php.net/manual/en/simplexmlelement.construct.php
Gordon
@Phill or replace the call with `simplexml_load_file`
Gordon
Hmm, made the change: $this->xml = new SimpleXMLElement($xml, NULL, TRUE); but still getting error: PHP Fatal error: Uncaught exception 'Exception' with message 'String could not be parsed as XML' I made the change in the class example you have, is this correct?
Phill Pafford
"@Phill or replace the call with simplexml_load_file" replace where? In the constructor?
Phill Pafford
@Phill change `$this->xml = new SimpleXMLElement($xml);` to `$this->xml = new simplexml_load_file($xml);` If the error keeps coming then, the path is likely incorrect. Try to verify if you can open the path with `file_get_contents()` and see what it contains.
Gordon
ok thats what I did and then got this: PHP Fatal error: Class 'simplexml_load_file' not found in
Phill Pafford
@Phill sorry. Remove the new keyword from the function call
Gordon
LOL, I miss that as well. Thnx again
Phill Pafford
Hey Gordon I had one other question. See Edit
Phill Pafford
Thanks I got it
Phill Pafford
@Phill Hmm, I think so. Try "//process[@id='$id' and @name='$name']" for an XPath then
Gordon
I tried the AND but no go, I did find the solution though. //process[@id='$id'][@name='$name']
Phill Pafford
@Phill ah ok. Right on! :)
Gordon