Simple XMLElement Object
(
[IpStatus] => 1
[ti_pid_20642] => SimpleXmlElement Object
(
I have a SimpleXMLElment in above format and this XML is generated at run time and it's node values like ti_pid_20642 are partly dnymaic, for example ti_pid_3232, ti-pid_2323, ti_pid_anyumber.
My question is how can I get these nodes values and it's children using PHP?
To get all node names that are used in an XML string with SimpleXML you can use the SimpleXMLIterator:
$tagnames = array_keys(iterator_to_array(
new RecursiveIteratorIterator(
new SimpleXMLIterator($string)
, RecursiveIteratorIterator::SELF_FIRST
)
));
print_r($tagnames);
Which could give you exemplary (you did not give any XML in your question, Demo):
Array
(
[0] => IpStatus
[1] => ti_pid_20642
[2] => dependend
[3] => ti-pid_2323
[4] => ti_pid_anyumber
[5] => more
)
If you have problems to provide a string that contains valid XML, take your existing SimpleXMLelement and create an XML string out of it:
$string = $simpleXML->asXML();
However, if you like to get all tagnames from a SimpleXML object but you don't want to convert it to a string, you can create a recursive iterator for SimpleXMLElement as well:
class SimpleXMLElementIterator extends IteratorIterator implements RecursiveIterator
{
private $element;
public function __construct(SimpleXMLElement $element) {
parent::__construct($element);
}
public function hasChildren() {
return (bool)$this->current()->children();
}
public function getChildren() {
return new self($this->current()->children());
}
}
The usage of it would be similar (Demo):
$it = new RecursiveIteratorIterator(
new SimpleXMLElementIterator($xml), RecursiveIteratorIterator::SELF_FIRST
);
$tagnames = array_keys(iterator_to_array($it));
It just depends on what you need.
This becomes less straight forward, with namespaced elements. Depending if you want to get the local names only or the namspace names or even the namespace URIs with the tagnames.
The given SimpleXMLElementIterator could be changed to support the iteration over elements across namespaces, by default simplexml only offers traversal over elements in the default namespace:
/**
* SimpleXMLElementIterator over all child elements across namespaces
*/
class SimpleXMLElementIterator extends IteratorIterator implements RecursiveIterator
{
private $element;
public function __construct(SimpleXMLElement $element) {
parent::__construct(new ArrayIterator($element->xpath('./*')));
}
public function key() {
return $this->current()->getName();
}
public function hasChildren() {
return (bool)$this->current()->xpath('./*');
}
public function getChildren() {
return new self($this->current());
}
}
You would then need to check for the namespace per each element- As an example a modified XML document making use of namespaces:
<root xmlns="namspace:default" xmlns:ns1="namespace.numbered.1">
<ns1:IpStatus>1</ns1:IpStatus>
<ti_pid_20642>
<dependend xmlns="namspace:depending">
<ti-pid_2323>ti-pid_2323</ti-pid_2323>
<ti_pid_anyumber>ti_pid_anyumber</ti_pid_anyumber>
<more xmlns:ns2="namspace.numbered.2">
<ti_pid_20642 ns2:attribute="test">ti_pid_20642</ti_pid_20642>
<ns2:ti_pid_20642>ti_pid_20642</ns2:ti_pid_20642>
</more>
</dependend>
</ti_pid_20642>
</root>
Combined with the update SimpleXMLIterator above the following example-code demonstrates the new behavior:
$xml = new SimpleXMLElement($string);
$it = new RecursiveIteratorIterator(
new SimpleXMLElementIterator($xml), RecursiveIteratorIterator::SELF_FIRST
);
$count = 0;
foreach ($it as $name => $element) {
$nsList = $element->getNamespaces();
list($ns, $nsUri) = each($nsList);
printf("#%d: %' -20s %' -4s %s\n", ++$count, $name, $ns, $nsUri);
}
Output (Demo):
#1: IpStatus ns1 namespace.numbered.1
#2: ti_pid_20642 namspace:default
#3: dependend namspace:depending
#4: ti-pid_2323 namspace:depending
#5: ti_pid_anyumber namspace:depending
#6: more namspace:depending
#7: ti_pid_20642 namspace:depending
#8: ti_pid_20642 ns2 namspace.numbered.2
Have fun.
Related
I need to be able to create strict typed maps dynamically. Like this:
$map = new Map( 'string,array<string,int>', [
'foo' => [
'bar' => 1
]
];
I have seen a lot of solutions for separate cases. All guides are teaching to create a class for each map, like Users_Map (to keep users there), Products_Map (to keep products there), Comments_Map (to keep comments there), etc.
But I don't want to have 3 classes (dozens in fact - for a big project) for each type of the map. I want to create a single class Map and then use it like this:
$users = new Map( 'User', {users data goes here} );
$products = new Map( 'int,Product', {products data goes here} );
$comments = new Map( 'User,array<Comment>', {comments data goes here} );
I would appreciate if somebody can advice me any existing repos. Otherwise I'll probably implement this on my own and will put here a link to my solution as an answer.
What you're looking for is called generics. PHP doesn't support this, although there has been an RFC calling for support for a few years.
If you really want to enforce strict typing on a custom map, you'd have to build it yourself. You could, for example, do something like this:
class Map {
private string $keyType;
private string $valueType;
private array $items;
public function __construct(string $keyType, string $valueType) {
$this->keyType = $keyType;
$this->valueType = $valueType;
}
public function set($key, $value) {
if (gettype($key) !== $this->keyType && !($key instanceof $this->keyType)) {
throw new TypeError("Key must be of type " . $this->keyType);
}
if (gettype($value) !== $this->valueType && !($value instanceof $this->valueType)) {
throw new TypeError("Value must be of type " . $this->valueType);
}
$this->items[$key] = $value;
}
public function get($key) {
if (gettype($key) !== $this->keyType) {
throw new TypeError("Key must be of type " . $this->keyType);
}
return $this->items[$key] ?? null;
}
public function all() {
return $this->items;
}
}
(of course, this particular implementation uses a regular array internally, so keyType is limited to types that are valid array keys. If you want to support other object types, some more interesting logic might be required)
The combination of gettype and instanceof will ensure this works for both simple and complex types. For example:
$map = new Map("string", "array");
$map->set("name", ["Boris", "Johnson"]);
print_r($map->all());
/*
Array
(
[name] => Array
(
[0] => Boris
[1] => Johnson
)
)
*/
$map->set("job", "Prime Minister");
// Fatal error: Uncaught TypeError: Value must be of type array
Or with a class as value type:
class User {
public string $firstName;
public string $lastName;
}
$user = new User();
$user->firstName = "Boris";
$user->lastName = "Johnson";
$map = new Map("string", User::class);
$map->set("pm", $user);
print_r($map->all());
/*
Array
(
[pm] => User Object
(
[firstName] => Boris
[lastName] => Johnson
)
)
*/
If you also want to support nested generics, like in your example array<string,int>, that becomes more complicated. In that case, as soon as someone passes an array as a value, you'd have to manually check all items in the array to ensure all array keys are strings and all array values are integers. It's possible, but for larger arrays it will be a significant performance hit.
Although you could use a nested Map like this one if you extend it to enforce the types:
class StringIntMap extends Map {
public function __construct() {
parent::__construct("string", "integer");
}
}
$map = new Map("string", StringIntMap::class);
I'm using reflection to dynamically call methods.
$object = new $class;
$reflector = new ReflectionMethod($class, $method);
$reflector->invokeArgs($object, $arguments);
The $arguments array looks like:
Array
(
[fooparam] => false
[id] => 238133
)
The method called could be:
class MyClass
{
public function myMethod ($id, $fooParam)
{
// Whatever
}
}
The problem is that everything comes from frontend designers, depending on data-* attributes, href... so $arguments array has arbitrary sorting.
How can I sort this array to match method parameters?
O maybe, is there a better solution? Named parameters?
Use ReflectionMethod::getParameters() to get a list of arguments and filter map them to their corresponding position, e.g.:
$sorted_args = array_map(function($param) use($arguments) {
$name = $param->getName();
if (!isset($arguments[$name]) && !$param->isOptional())
throw new BadMethodCallException("Argument '{$name}' is mandatory");
return isset($arguments[$name]) ? $arguments[$name] : $param->getDefaultValue();
}, $reflector->getParameters());
You could also use a simple foreach loop, it's up to you.
Then invoke the method with $sorted_args instead:
$reflector->invokeArgs($object, $sorted_args);
$arrResult=array(
0=>array('categoryid'=>112,'catname'=>'apperal','subcategory'=>array(
412=>array('categoryid'=>428,'catname'=>'rainwear','subcategory'=>array(
428=>array('categoryid'=>413,'catname'=>'summer','subcategory'=>array()))))));
print_r($arrResult);
$iterator = new RecursiveArrayIterator($arrResult);
iterator_apply($iterator, 'traverseStructure', array($iterator));
function traverseStructure($iterator) {
$arrAddResult=array('categoryid'=>416,'catname'=>'winter','subcategory'=>array());
while ( $iterator -> valid() ) {
if ( $iterator -> hasChildren() ) {
traverseStructure($iterator -> getChildren());
}
else {
if($iterator -> current() == 413)
{
$arr=&$iterator;
$a='arr';
${$a}['subcategory']=$arrAddResult;
break;
}
}
$iterator -> next();
}
}
the expected output is to append the 'arrAddResult' appenedn in $arrResult. But with some reason the iterator get modify but it doesn't reflect the modification in arrResult array.
I tried passing the array by ref in function 'traverseStructure' but still struggling to get the correct output.
I am trying iterator first. I have to constructor a N-Level associative array as arrResult hence opt to use the iterator.
Here's an example on a way of doing this with one array.
<?php
$arrResult=array(
1=>array('categoryid'=>112,'catname'=>'apperal','subcategory'=>array()),
0=>array('categoryid'=>112,'catname'=>'apperal','subcategory'=>array(
1=>array('categoryid'=>112,'catname'=>'rainwear','subcategory'=>array(
1=>array('categoryid'=>112,'catname'=>'apperal','subcategory'=>array()),
428=>array('categoryid'=>413,'catname'=>'summer','subcategory'=>array()))
),
412=>array('categoryid'=>428,'catname'=>'rainwear','subcategory'=>array(
1=>array('categoryid'=>112,'catname'=>'apperal','subcategory'=>array()),
428=>array('categoryid'=>413,'catname'=>'summer','subcategory'=>array()))
)
)
)
);
function append(&$ar,$who,$what){
// just a simple check, you can remove it
if(!is_array($ar))return false;
// loop through all keys
foreach($ar as $k=>$v){
// found node, i'm assuming you don't have the node multiple times
// if you want this to go forever, remove the returns and the if on the add()
if($v['categoryid']==$who){
$ar[$k]['subcategory'][]=$what;
return true;
}
// recursion !
if(add($ar[$k]['subcategory'],$who,$what))return true;// if found stop
}
// key not found here in this node or subnodes
return false;
}
append($arrResult,413,array('categoryid'=>416,'catname'=>'winter','subcategory'=>array()));
echo'<pre>';
var_dump($arrResult);
This might be inefficient on large arrays. I'd recommend making a class that caches the $who and $what so it doesn't get copied to all the levels of the traversal. The rest should be identical.
I was wondering if a function capable of converting an associative array to an XML document exists in PHP (or some widely available PHP library).
I've searched quite a lot and could only find functions that do not output valid XML. I believe that the array I'm testing them on is correctly constructed, since it can be correctly used to generate a JSON document using json_encode. However, it is rather large and it is nested on four levels, which might explain why the functions I've tried so far fail.
Ultimately, I will write the code to generate the XML myself but surely there must be a faster way of doing this.
I realize I am a Johnny-Come-Lately here, but I was working with the VERY same problem -- and the tutorials I found out there would almost (but not quite upon unit testing) cover it.
After much frustration and research, here is what I cam up with
XML To Assoc. Array:
From http://www.php.net/manual/en/simplexml.examples-basic.php
json_decode( json_encode( simplexml_load_string( $string ) ), TRUE );
Assoc. Array to XML
notes:
XML attributes are not handled
Will also handle nested arrays with numeric indices (which are not valid XML!)
From http://www.devexp.eu/2009/04/11/php-domdocument-convert-array-to-xml/
/// Converts an array to XML
/// - http://www.devexp.eu/2009/04/11/php-domdocument-convert-array-to-xml/
/// #param <array> $array The associative array you want to convert; nested numeric indices are OK!
function getXml( array $array ) {
$array2XmlConverter = new XmlDomConstructor('1.0', 'utf-8');
$array2XmlConverter->xmlStandalone = TRUE;
$array2XmlConverter->formatOutput = TRUE;
try {
$array2XmlConverter->fromMixed( $array );
$array2XmlConverter->normalizeDocument ();
$xml = $array2XmlConverter->saveXML();
// echo "\n\n-----vvv start returned xml vvv-----\n";
// print_r( $xml );
// echo "\n------^^^ end returned xml ^^^----\n"
return $xml;
}
catch( Exception $ex ) {
// echo "\n\n-----vvv Rut-roh Raggy! vvv-----\n";
// print_r( $ex->getCode() ); echo "\n";
// print_r( $->getMessage() );
// var_dump( $ex );
// echo "\n------^^^ end Rut-roh Raggy! ^^^----\n"
return $ex;
}
}
... and here is the class to use for the $array2XmlConverter object:
/**
* Extends the DOMDocument to implement personal (utility) methods.
* - From: http://www.devexp.eu/2009/04/11/php-domdocument-convert-array-to-xml/
* - `parent::` See http://www.php.net/manual/en/class.domdocument.php
*
* #throws DOMException http://www.php.net/manual/en/class.domexception.php
*
* #author Toni Van de Voorde
*/
class XmlDomConstructor extends DOMDocument {
/**
* Constructs elements and texts from an array or string.
* The array can contain an element's name in the index part
* and an element's text in the value part.
*
* It can also creates an xml with the same element tagName on the same
* level.
*
* ex:
\verbatim
<nodes>
<node>text</node>
<node>
<field>hello</field>
<field>world</field>
</node>
</nodes>
\verbatim
*
*
* Array should then look like:
\verbatim
array(
"nodes" => array(
"node" => array(
0 => "text",
1 => array(
"field" => array (
0 => "hello",
1 => "world",
),
),
),
),
);
\endverbatim
*
* #param mixed $mixed An array or string.
*
* #param DOMElement[optional] $domElement Then element
* from where the array will be construct to.
*
*/
public function fromMixed($mixed, DOMElement $domElement = null) {
$domElement = is_null($domElement) ? $this : $domElement;
if (is_array($mixed)) {
foreach( $mixed as $index => $mixedElement ) {
if ( is_int($index) ) {
if ( $index == 0 ) {
$node = $domElement;
}
else {
$node = $this->createElement($domElement->tagName);
$domElement->parentNode->appendChild($node);
}
}
else {
$node = $this->createElement($index);
$domElement->appendChild($node);
}
$this->fromMixed($mixedElement, $node);
}
}
else {
$domElement->appendChild($this->createTextNode($mixed));
}
}
} // end of class
No. At least there is no such in-built function. It's not a probrem to write it at all.
surely there must be a faster way of doing this
How do you represent attribute in array? I can assume keys are tags and values are this tags content.
Basic PHP Array -> JSON works just fine, cause those structure is... well... almost the same.
Call
// $data = array(...);
$dataTransformator = new DataTransformator();
$domDocument = $dataTransformator->data2domDocument($data);
$xml = $domDocument->saveXML();
DataTransformator
class DataTransformator {
/**
* Converts the $data to a \DOMDocument.
* #param array $data
* #param string $rootElementName
* #param string $defaultElementName
* #see MyNamespace\Dom\DataTransformator#data2domNode(...)
* #return Ambigous <DOMDocument>
*/
public function data2domDocument(array $data, $rootElementName = 'data', $defaultElementName = 'item') {
return $this->data2domNode($data, $rootElementName, null, $defaultElementName);
}
/**
* Converts the $data to a \DOMNode.
* If the $elementContent is a string,
* a DOMNode with a nested shallow DOMElement
* will be (created if the argument $node is null and) returned.
* If the $elementContent is an array,
* the function will applied on every its element recursively and
* a DOMNode with a nested DOMElements
* will be (created if the argument $node is null and) returned.
* The end result is always a DOMDocument object.
* The casue is, that a \DOMElement object
* "is read only. It may be appended to a document,
* but additional nodes may not be appended to this node
* until the node is associated with a document."
* See {#link http://php.net/manual/en/domelement.construct.php here}).
*
* #param Ambigous <string, mixed> $elementName Used as element tagname. If it's not a string $defaultElementName is used instead.
* #param Ambigous <string, array> $elementContent
* #param Ambigous <\DOMDocument, NULL, \DOMElement> $parentNode The parent node is
* either a \DOMDocument (by the method calls from outside of the method)
* or a \DOMElement or NULL (by the calls from inside).
* Once again: For the calls from outside of the method the argument MUST be either a \DOMDocument object or NULL.
* #param string $defaultElementName If the key of the array element is a string, it determines the DOM element name / tagname.
* For numeric indexes the $defaultElementName is used.
* #return \DOMDocument
*/
protected function data2domNode($elementContent, $elementName, \DOMNode $parentNode = null, $defaultElementName = 'item') {
$parentNode = is_null($parentNode) ? new \DOMDocument('1.0', 'utf-8') : $parentNode;
$name = is_string($elementName) ? $elementName : $defaultElementName;
if (!is_array($elementContent)) {
$content = htmlspecialchars($elementContent);
$element = new \DOMElement($name, $content);
$parentNode->appendChild($element);
} else {
$element = new \DOMElement($name);
$parentNode->appendChild($element);
foreach ($elementContent as $key => $value) {
$elementChild = $this->data2domNode($value, $key, $element);
$parentNode->appendChild($elementChild);
}
}
return $parentNode;
}
}
PHP's DOMDocument objects are probably what you are looking for. Here is a link to an example use of this class to convert a multi-dimensional array into an xml file - http://www.php.net/manual/en/book.dom.php#78941
function combArrToXML($arrC=array(), $root="root", $element="element"){
$doc = new DOMDocument();
$doc->formatOutput = true;
$r = $doc->createElement( $root );
$doc->appendChild( $r );
$b = $doc->createElement( $element );
foreach( $arrC as $key => $val)
{
$$key = $doc->createElement( $key );
$$key->appendChild(
$doc->createTextNode( $val )
);
$b->appendChild( $$key );
$r->appendChild( $b );
}
return $doc->saveXML();
}
Example:
$b=array("testa"=>"testb", "testc"=>"testd");
combArrToXML($b, "root", "element");
Output:
<?xml version="1.0"?>
<root>
<element>
<testa>testb</testa>
<testc>testd</testc>
</element>
</root>
surely there must be a faster way of doing this
If you've got PEAR installed, there is. Take a look at XML_Seralizer. It's beta, so you'll have to use
pear install XML_Serializer-beta
to install
I needed a solution which is able to convert arrays with non-associative subarrays and content which needs to be escaped with CDATA (<>&). Since I could not find any appropriate solution, I implemented my own based on SimpleXML which should be quite fast.
https://github.com/traeger/SimplestXML (this solution supports an (Associative) Array => XML and XML => (Associative) Array conversion without attribute support). I hope this helps someone.
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.
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;
}
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:
http://www.w3schools.com/XPath/default.asp
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:
http://www.phpro.org/tutorials/Introduction-to-SPL.html