Manipulando XML de Maneira Fácil no CodeIgniter

ci-easy-xml-logo

Eu conduzi um projeto de um site 100% em Flash para um loja de móveis planejados. O requisito principal deste projeto foi com relação em dar total autonomia do gerenciamento do site para o cliente. O site em Flash consultava vários arquivos XML para cada seção do site: notícias, galeria de fotos, background, produtos, etc. Assim desenvolvi um painel administrativo em CodeIgniter (PHP) para fazer todo o CRUD (Create, Read, Update e Delete) do site. Deste modo o cliente pode listar todo o conteúdo de uma determinada seção, alterar, inserir e deletar o material do site que conforme mencionado, é em Flash.

Para tornar a parte de desenvolvimento mais simples resolvi criar uma biblioteca para o CodeIgniter para simular o mais próximo possível uma transação com o banco de dados (ex. MySQL). Da mesma forma que utilizamos as classes DAO dentro do diretório "models" do CodeIgniter a biblioteca que desenvolvi tem por objetivo facilitar a manipulação de arquivos XML com a mesma transparência  de uma classe DAO. A biblioteca já disponibilizei no GitHub e o nome dela é CI EasyXML. Esta biblioteca é uma camada de abstração entre o controller e o arquivo XML, assim no controller você não precisa manipular o XML diretamente. Para verificar e baixar a biblioteca no repositório do GitHub clique aqui. Abaixo vou colocar um exemplo de como utilizar o CI EasyXML no seu framework do CodeIgniter. O exemplo que posto abaixo é o mesmo que está no GitHub.

O primeiro passo é ter a biblioteca salva na pasta libraries do seu projeto. Caso você não fez alterações no framework do CodeIgniter a pasta libraries fica abaixo da pasta "application". O nome do arquivo é "Ci_easyxml.php" e o conteúdo segue abaixo como ilustração, mas sugiro baixar tudo do GitHub:

[php] <? if (! defined ( 'BASEPATH' ))exit ( 'No direct script access allowed' ); /** * Class Ci_easyxml. * * This class is an abstraction layer between controllers and * XML files. This class has the aim to behave like CodeIgniter * DAO models. * * @author Carlos Eduardo da Silva <carlosedasilva@gmail.com> * @package libraries * @link https://blog.tetranet.com.br/category/php/codeigniter/ * @version 1.0 * * @todo Make this class generic. The specific data related to * a XML file should be inside models no here. Next versions of * of this class the DAO and VO classes should be inside models * folder and this class have to be instantiated inside the model * files not directly from controllers. */ class Ci_easyxml { private $xml = null; private $path = null; /** * Constructor * * Constructor to validate the environment and load the xml file. * * @access public * @param mixed $params path = '/database/db_test.xml' */ function __construct( $params ) { if ( file_exists ( $params ['path'] )) { if( extension_loaded('simplexml') ) { if( is_writable($params ['path']) ) { $this->path = $params ['path']; if( ! $this->xml = simplexml_load_file( $params ['path'], 'SimpleXMLExtended' ) ) { // Problems :( show_error('Error reading '.basename( $params ['path'] ).' file.'); } } else { // Problems :( show_error('The file '.basename( $params ['path'] ).' is not writable! Check the permissions.'); } } else { // Problems :( show_error('SimpleXML is not loaded! Check the loaded modules you have: <pre>' . var_export( get_loaded_extensions(), true ) . '</pre>' ); } } else { // Problems :( show_error('Some featutres of EasyXML wont work, because the XML file does\'t exist! Check the path you informed: ' . $params ['path'] ); } } /** * Get Num Elements * * Return the number of nodes in XML file. * * @access public * @return int The number of elements. * * @example $this->ci_easyxml->get_num_elements(); */ public function get_num_elements() { $count = 0; foreach ( $this->xml as $node ) { $count ++; } return $count; } /** * Child Exists * * Check if a particular child exists in the xml file. * * @access public * @param string $childpath Name of the child node. * * @return mixed If true return the number of childs or else return false. * * @example $this->ci_easyxml->child_exists('/catalog/book/author'); */ public function child_exists( $childpath ) { $result = $this->xml->xpath($childpath); return count( $result ) ? $result : false; } /** * Attribute Value Exists * * Check if a certain value of an attribute exists in XML file. * * @access public * @param string $childpath Name of the node. * @param string $attribute Name of the vaule. * @param string $atribvalue Value of the attribute. * * @return mixed If true return the number of values or else return false. * * @example $this->ci_easyxml->attribute_value_exists('/catalog/book','id','bk103'); */ public function attribute_value_exists( $childpath, $attribute, $atribvalue ) { $xpath_val = "//".$childpath."[@".$attribute."='".$atribvalue."']"; $res = $this->xml->xpath( $xpath_val ); return count( $res ) ? $res : false; } /** * Remove Node By Attribute * * Remove a node from xml file by its attribute. * * @access public * @param string $nodename Name of the node. * @param string $attribute Name of the value. * @param string $value Value of the attribute. * * @return boolean TRUE: Delete success. FALSE: Delete errror. * * @example $this->ci_easyxml->removeNodeByAttrib('book','id','bk113'); */ public function removeNodeByAttrib( $nodename, $attribute, $value ) { if( $this->attribute_value_exists('/catalog/'.$nodename, $attribute, $value) ) { $xpath_val = "//".$nodename."[@".$attribute."='".$value."']"; $res = $this->xml->xpath($xpath_val); foreach( $res as $key => $node ) { foreach( $node->attributes() as $attrib => $val ) { if( ($attrib == $attribute) && ($val == $value) ) { $oNode = dom_import_simplexml( $node ); } } } $oNode->parentNode->removeChild( $oNode ); $this->xml->saveXMLIndented( $this->xml, $this->path ); return TRUE; } else { return FALSE; } } /** * Force conversion from ISO8859-1 to UTF-8. * * It's a problem for non-english websites/systems to handle XML in * other languages. So this method tries to give a hand converting * texts in iso8859-1 to UTF-8. * * @access private * @param string $text The text to be converted. * * @return string The string in UTF-8 encode. * * @example $content = $this->forceIso8859toUTF8( $content ); */ private function forceIso8859toUTF8( $text ) { return iconv( "ISO-8859-1", "UTF-8//TRANSLIT", $text ); } /** * Get Max Attribute * * Returns the maximum value of an attribute. * * @access private * @param string $childpath Name of the node. * @param string $attribute Name of the attribute. * * @return mixed The maximum value of an attribute. * * @example $this->get_max_attribute('/catalog/book','id'); */ private function get_max_attribute( $childpath, $attribute) { $xpath_val = "//".$childpath."[@".$attribute."]"; $res = $this->xml->xpath( $xpath_val ); foreach( $res as $key => $node ) { foreach( $node->attributes() as $attrib => $val ) { if( ($attrib == $attribute) ) { $arr[] = preg_replace("/[^0-9]/","", $val); } } } return max($arr); } /** * Insert Book * * Inserts a new book node in xml file. * * @param array $data Book information. * @return int The Id of inserted book. * * @example $this->ci_easyxml->insert_book($arrayChildNodes); */ public function insert_book( $data ) { $book = $this->xml->addChild('book'); $last_id = $this->get_max_attribute('/catalog/book','id') + 1; $last_id = 'bk'.$last_id; $book->addAttribute('id', $last_id); foreach($data as $key => $value) { $book->addChild($key,$value); } $this->xml->saveXMLIndented( $this->xml, $this->path ); return $last_id; #returning last id } /** * Update Book * * Update a book node. * * @param array $data Book information, including its Id. * @return boolean TRUE: Success. FALSE: Error. * * @example $this->ci_easyxml->update_book($arrayChildNodes); */ public function update_book( $data ) { $res = $this->attribute_value_exists( "book", "id", $data['id'] ); if( $res ) { $res[0]->author = $data['author']; $res[0]->title = $data['title']; $res[0]->genre = $data['genre']; $res[0]->price = $data['price']; $res[0]->publish_date = $data['publish_date']; $res[0]->description = $data['description']; $this->xml->saveXMLIndented( $this->xml,$this->path ); return TRUE; } else { return FALSE; } } /** * Ensures UTF-8 * * Method to ensure if a string is in UTF-8. * * @param string $string A text to be verified. * @return string The text in UTF-8. */ private function ensureUTF8($string) { $encoding = mb_detect_encoding($string); if($encoding != 'UTF-8') { return iconv($encoding, 'UTF-8//TRANSLIT', $string); } else { return $string; } } } // end_class /** * SimpleXMLExtended * * Adding extra spice in PHP SimpleXML. * * @author Carlos */ class SimpleXMLExtended extends SimpleXMLElement { /** * Add CData * * Adds Cdata in XML, useful if your string contains HTML tags. * * @param string $cdata_text String containing html tags. * * @example $my_child = $item->addChild ( 'foo' ); * $my_child->addCData( '<strong><u>Hello World!!</u></strong>' ); */ public function addCData($cdata_text) { $node = dom_import_simplexml ( $this ); $no = $node->ownerDocument; $node->appendChild ( $no->createCDATASection ( $cdata_text ) ); } /** * Saves a XML Indented file. * * Saves an indented XML file. * * @param object $xml XML object. * @param string $path XML path in file system. * * @example $this->xml->saveXMLIndented( $this->xml,$this->path ); */ public function saveXMLIndented( $xml, $path = "" ) { $doc = new DOMDocument ( '1.0' ); $doc->preserveWhiteSpace = false; $doc->loadXML ( $xml->asXML() ); $doc->formatOutput = true; if( $path == "" ) { echo $doc->saveXML(); } else { file_put_contents( $path, $doc->saveXML()); } } } //end_class #EOF [/php]

O segundo passo é ter salvo em algum lugar do seu projeto o arquivo XML que você deseja manipular. Como encaro o arquivo XML aqui como um banco de dados, resolvi criar uma pasta abaixo de "application" chamada "database" e dentro dela deixo salvo o arquivo XML. Para o exemplo que acompanha o pacote da biblioteca estou utilizando o seguinte arquivo em XML disponibilizado no site da Microsoft aqui. Quis utilizar este arquivo em XML para trazer um exemplo mais real sem ter inventado algum XML artificial que funcionasse para esta biblioteca, assim pegando um arquivo externo é mais uma prova de que a biblioteca funciona bem.

O terceiro passo, é você utilizar a biblioteca em seu controller, abaixo estou colocando um exemplo bem simplificado de como utilizar o CI EasyXML na prática. No controller abaixo o nome do arquivo é "test.php" e tenta utilizar a maioria dos métodos disponíveis no CI EasyXML.

[php] <? if (! defined ( 'BASEPATH' ))exit ( 'No direct script access allowed' ); /** * Controller aimed to test Ci_easyxml library. * @author Carlos * */ class Test extends Controller { function __construct() { parent::Controller(); } function index() { $params = array('path' => './database/db_test.xml'); $this->load->library('ci_easyxml',$params); # Loading the library with the xml file path. echo $this->ci_easyxml->get_num_elements(); # Returning the number of elements. // Verifying if the child "author" in the path "/catalog/book/author" exists or not. if( $childs = $this->ci_easyxml->child_exists('/catalog/book/author') ) { echo "<br><br>The child exists.<br><pre>"; print_r($childs); echo "</pre><hr>"; } else { echo "<br>The child does not exists."; } // Verifying if the value "id" in the path "/catalog/book" exists or not. if( $value = $this->ci_easyxml->attribute_value_exists('/catalog/book','id','bk103') ) { echo "<br><br>The value exists.<br><pre>"; print_r($value); echo "</pre><hr>"; } else { echo "<br>The value does not exists."; } // Removing a node based on its attribute value. //$this->ci_easyxml->removeNodeByAttrib('book','id','bk113'); # Uncomment to delete the node. // Inserting new book $bk['author'] = "Test Author"; $bk['title'] = "Test Title"; $bk['genre'] = "Test Genre"; $bk['price'] = "0.00"; $bk['publish_date'] = "00-00-0000"; $bk['description'] = "Test Description"; #$bk_last_id = $this->ci_easyxml->insert_book($bk); # Uncomment to insert a new book. #echo "<br>" . $bk_last_id . "<br>"; // UPDATING BOOK $bk['id'] = "bk114"; # BOOK ID $bk['author'] = "Test Author EDIT"; $bk['title'] = "Test Title EDIT"; $bk['genre'] = "Test Genre EDIT"; $bk['price'] = "1.00"; $bk['publish_date'] = "00-00-2013"; $bk['description'] = "Test Description EDIT"; #$bk_id = $this->ci_easyxml->update_book($bk); # Uncomment to update a book. #echo "<br>"; #print_r($bk_id); #echo "<br>"; } } [/php]

O código está bem comentado dispensado maiores explicações. Maiores dúvidas deixe seu comentário, também abri uma nova thread no fórum do CodeIgniter aqui. Dentro da library deixei listado no comentário inicial da classe em "@todo" os próximos passos de atualização e melhoria desta biblioteca. Que seria entre outros pontos deixar ela mais genérica possível e a parte específica inerente a cada arquivo XML utilizado ficar armazenado na pasta models, sendo que a a biblioteca seja carregada dentro de cada model, dispensando o carregamento dela dentro do controller, somente do model, da mesma forma que é feito para o banco de dados. É isto, caso você não utilize o CodeIgniter com poucas alterações nesta biblioteca você poderá utilizar em outros frameworks. Até a próxima!