PHP/XML Data Binding, Serialization & Deserialization

November 12th, 2009

One of the things I’ve had to deal with a lot lately is XML serialization & de-serialization.  I recently wrote myself a Facebook app that profiles my latest blog posts in the sidebar, and had to serialize some data into XML for a Flash/ActionScript project I’m doing.

The biggest problem with trying to do any of this is really that all the solutions I found out there that were ‘ready made’ also included taking on an entire framework, which I’m not a big fan of. So, I wrote my own functions to do this.

It’s a pretty simple abstract class that you can then just call a toXml() method, and it returns a nice XML tree. The reverse deserializer works by instantiating your class, and then just passing it an xml tree. It then it just does some basic XML unmarshalling to bind your nodes to their respective objects.

This is a first-jab at it, and I realize there are clearly some optimizations I can make by changing it to pass by reference, as well as having it pass the recursion on to a referenced class, but I figured I should post it in case anyone else out there is having this issue. There’s also some sample code.

For the impatient (note lines 56 & 71 got munged by the code plug-in I’m using for wordpress and should be close tags: </$something>):


	 *    1
	 *    ...
	 *    n
	 *  
	 * 
	 * @param object $obj [optional] initially null, the recursion will populate
	 * @return String A string of an XML tree for this object.
	 */
	public function asXml($obj= null) 
	{
		if($obj== null){ $obj= $this; }
		
		$className= get_class($obj);
		$nodeName= strtolower($className);
		$xml= "<$nodeName>\n\t";
		
	    $ref = new ReflectionClass($obj);
		
		//iterate through a class's methods
	    foreach (array_values($ref->getMethods()) as $method) {
	    	
			//specifically the ones that start with "get"
	        if ((0 === strpos($method->name, "get"))
	                && $method->isPublic()) {
	            
	            $name = substr($method->name, 3);
	            $name[0] = strtolower($name[0]);
				
				// get the return value for that getter  
	            $value = $method->invoke($obj);
				
				// dig deeper for objects (as opposed to strings, ints, arrays)
				if("object" === gettype($value)) {
					
	                $value = $this->asXml($value);
					
	            } elseif("array" === gettype($value)) {
	            	
	            	// for arrays, we iterate through the children if they
					// are of type object, just as above
					$data= '';
					foreach($value as $obj){
						$data.= $this->asXml($obj);
					}
					
					$value= $data;
					
	            } else {
	            	
					// finally, we wrap everything in cdata nodes
					// to make things easier
	            	$value= "asNode($name, $value);
	        }
	    }
		
		$xml.= "\n\t";
		
	    return $xml;
	}
	
	/**
	 * Utility class for asXml()
	 * 
	 * @param object $name the name of the node
	 * @param object $value the value
	 * 
	 * @return A string representation of an XML node.
	 */
	private function asNode($name, $value)
	{
		return "<$name>$value\n\t";
	}
	
	//** DESERIALIZATION FUNCTIONS *****************************
	
	public function deserialize($xml)
	{
		$this->walkTree($xml, $this, '');
	}
		
	private function walkTree($xml, $obj, $t)
	{
		if($xml->hasChildNodes()){
			
			$rc= new ReflectionClass($obj);
			$childNodes= $this->getElementChildNodes($xml);
			
			foreach($childNodes as $node){
				
				if($node instanceof DOMElement){
					
					$tagName= $node->tagName; 
					$value= null;
					
					// Inspect deeper if there are child nodes
					$children= $this->getElementChildNodes($node);
					if($children){
						
						$className= ucfirst($tagName);
						$classFile= getcwd(). '/'. $className. '.php';
						
						// If a class exists for this node, then inspect
						// it for further children
						if(file_exists( $classFile )){
							
							$class= new $className();
							
							$value= $this->walkTree($node, $class, $t."\t");
							$this->invokeSetter($obj, $value, $tagName, $rc);
							
						} else {
							
							// If no class exists for this node, then
							// assume that it is a setter for a child one level
							// deeper, which *is* a class
							$childrenArray= array();
							
							foreach($children as $childNode){
								
								$childTagName= $childNode->tagName;
								$childClassName= ucfirst($childTagName);
								$childClassFile= getcwd(). '/'. $childClassName. '.php';
								
								if(file_exists( $childClassFile )){
									
									$childObj= new $childClassName();
									$childObj= $this->walkTree($childNode, $childObj, $t."\t");
									$childrenArray[]= $childObj;
									
								}
								
								
							}
							
							$value= $childrenArray;
							$this->invokeSetter($obj, $value, $tagName, $rc);

						}
						
					} else {
						
						// If there are no child nodes, then just 
						// set the value
						$value= $node->textContent;
						$this->invokeSetter($obj, $value, $tagName, $rc);
						
					}
						
				}
			}
			
			return $obj;
		}
		
	}
	
	private function invokeSetter(&$obj, $value, $tagName, $rc)
	{
		$methodName= 'set'. ucfirst($tagName);
		
		if($rc->hasMethod($methodName)){
		
			$method= $rc->getMethod($methodName);
			$method->invoke($obj, $value);
		
		}
	}
	
	private function getChildrenByTagname($elementNode, $tagname)
	{
		$nodes= $elementNode->childNodes; 
		$childNodes= array();
		
		foreach($nodes as $node){  

			if($node instanceof DOMElement && $node->tagName== $tagname)
			{
				$childNodes[]= $node;
			}
		}
		
		return $childNodes;
	}
	
	private function getElementChildNodes($elementNode)
	{
		$nodes= $elementNode->childNodes; 
		$childNodes= array();
		
		foreach($nodes as $node){  

			if($node instanceof DOMElement)
			{
				$childNodes[]= $node;
			}
		}
		
		return $childNodes;
	}
		
	
}

?>


2 Responses to “PHP/XML Data Binding, Serialization & Deserialization”

  1. Daniel says:

    Hi,

    many thanks for your inspiration with this kind of stuff :-)

    I have created a new class to support a little bit more compability with php and xml.

    Ps. I thinks there is a bug withthe asNode function for objects (dublicated node names will be created just use the returned node tree without create a node)

    Kind regards
    Daniel

  2. admin says:

    Yeah, I think there’s a bug in there somewhere. I’m actually going to be doing some PHP code generation stuff again in the next couple of weeks and will post any new learnings. Thanks for the note and the tip!

Leave a Reply