XML_Query2XML

Generating XML data from SQL queries

Lukas Feiler
Copyright 2006 by Lukas Feiler

Table of Contents

Introduction

XML_Query2XML allows you to transform the records retrieved with one or more SQL SELECT queries into XML data. Very simple to highly complex transformations are supported. Is was written with performance in mind and can handle large amounts of data. No XSLT needed!

Both methods XML_Query2XML::getXML() and XML_Query2XML::getFlatXML() return an instance of DOMDocument. The class DOMDocument is provided by PHP5's built-in DOM API.

Requirements

XML_Query2XML requires

  • PHP5: XML_Query2XML heavily uses the new exception handling and object orientation features.
  • PHP5's built-in DOM API
  • PDO (PHP5's built-in database abstraction class) PEAR DB, PEAR MDB2 or ADOdb.
The following packages are optional:

Migrating from v0.6.x and v0.7.x to v1.x.x

The release 0.8.0 of XML_Query2XML is not backward compatible! Due to security considerations XML_Query2XML does not use the native function eval() anymore. Therefore

Proposed migration strategy:

  • Wherever you currently use the "!" prefix, use the new callback prefix "#" instead. The first argument passed to the callback function/method is always the current record ($record). You can supply additional static arguments by placing them within the braces, e.g. 'MyClass:myMethod(arg2, arg3)' will result in MyClass:myMethod() being called with the current record as the first, the string 'arg2' as the second and 'arg3' as the third argument. In most cases you will want to put whatever code you used after the "!" prefix into a separate function or static method. That function/method is what you call using the callback prefix "#".
  • The migration for $options['condition'] works similarly. Move the PHP code into a separate function/method and call it using the callback prefix "#".

XML_Query2XML::factory()

XML_Query2XML::factory($db)

This is the factory method that will return a new instance of XML_Query2XML. The argument passed to the factory method can be an instance of PDO, PEAR DB, PEAR MDB2, ADOdb, PEAR Net_LDAP, PEAR Net_LDAP2 or any class that extends XML_Query2XML_Driver

Database Drivers for PDO, PEAR MDB2, PEAR DB, ADOdb

XML_Query2XML has drivers for the database abstraction layers PDO, PEAR MDB2, PEAR DB and ADOdb.

Using PDO with XML_Query2XML works like this:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. $pdo new PDO('mysql://root@localhost/Query2XML_Tests');
  4. $query2xml XML_Query2XML::factory($pdo);
  5. ?>

Using MDB2 with XML_Query2XML works like this:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $mdb2 MDB2::factory('mysql://root@localhost/Query2XML_Tests');
  5. $query2xml XML_Query2XML::factory($mdb2);
  6. ?>

The same thing with PEAR DB looks like that:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'DB.php';
  4. $db DB::connect('mysql://root@localhost/Query2XML_Tests');
  5. $query2xml XML_Query2XML::factory($db);
  6. ?>

And again the same thing with ADOdb:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'adodb/adodb.inc.php';
  4. //require_once 'adodb/adodb-exceptions.inc.php';
  5. //require_once 'adodb/adodb-pear.inc.php';
  6. $adodb ADONewConnection('mysql');
  7. $adodb->Connect('localhost''root''''Query2XML_Tests');
  8. $query2xml XML_Query2XML::factory($adodb);
  9. ?>
Note that XML_Query2XML works with ADOdb with the default error handling (no additional include file), error handling using exceptions (adodb-exceptions.inc.php) and error handling using PEAR_Error (adodb-pear.inc.php).

I would recommend using MDB2 as it can be considered more advanced than DB and much better designed and documented than ADOdb. MDB2 also provides more flexibility than PDO. If you want to access a SQLite 3 database use PDO - MDB2 does only support SQLite 2 as of this writing. But use whichever you like - XML_Query2XML works with all of them. For the sake of simplicity all the examples will use PEAR MDB2.

LDAP Driver for PEAR Net_LDAP

Since v1.6.0RC1 XML_Query2XML comes with a driver for PEAR Net_LDAP. The driver for PEAR Net_LDAP2 is available since v1.7.0RC1.

Using Net_LDAP(2) with XML_Query2XML works like this:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. $ldap Net_LDAP::connect(
  4.     'host'     => 'ldap.example.com',
  5.     'port'     => 389,
  6.     'version'  => 3,
  7.     'starttls' => true,
  8.     'binddn'   => 'cn=Manager,ou=people,dc=example,dc=com',
  9.     'bindpw'   => 'secret'
  10. );
  11. $query2xml XML_Query2XML::factory($ldap);
  12. ?>
The driver for Net_LDAP(2) uses a diffrent format for $sql. Instead of a string it expects an associative array with the following elements:
  • 'base': the base search DN
  • 'filter': the query filter that determines which results are returned
  • 'options': an array of configuration options for the current query
More information on how to use the LDAP drivers can be found under The LDAP Driver

XML_Query2XML::getFlatXML()

XML_Query2XML::getFlatXML($sql, $rootTagName = 'root', $rowTagName = 'row')

This method transforms the data retrieved by a single SQL query into flat XML data. Pass the SQL SELECT statement as first, the root tag's name as second and the row tag's name as third argument.

In most cases you will want to use XML_Query2XML::getXML() instead. Please see Case 01: simple SELECT with getFlatXML for an example usage of getFlatXML().

XML_Query2XML::getXML()

XML_Query2XML::getXML($sql, $options)

This method is the most powerful transformation method. It returns an instance of DOMDocument (part of PHP5's built-in DOM API). The records returned by the query/queries will be processed one after another. The $options argument is a rather complex, associative, multi dimensional array. The $sql argument can be a string or as well an associative array.

$sql

This option is almost exactly like $options['sql']: you can specify the query with a Simple Query Specification or a Complex Query Specification. What is different from $options['sql'] is that you can also specify a boolean value of false.

Here is an example of a simple query specification (WARNING: to prevent SQL injection vulerabilities you should use a complex query specification when dealing with non-static queries like this one):

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. if (isset($_REQUEST['artistid']&& is_numeric($_REQUEST['artistid'])) {
  6.     $artistid =  $_REQUEST['artistid'];
  7. else {
  8.     $artistid 1;
  9. }
  10. $dom $query2xml->getXML(
  11.   "SELECT * FROM artist WHERE artistid = $artistid",
  12.   array(
  13.     'rootTag' => 'favorite_artist',
  14.     'idColumn' => 'artistid',
  15.     'rowTag' => 'artist',
  16.     'elements' => array(
  17.         'name',
  18.         'birth_year',
  19.         'music_genre' => 'genre'
  20.     )
  21.   )
  22. );
  23. header('Content-Type: application/xml');
  24. $dom->formatOutput true;
  25. print $dom->saveXML();
  26. ?>
With simple query specifications you have to prevent SQL injection yourself. Here I ensured that $artistid is numeric by calling is_numeric().

Next we use a Complex Query Specification and prevent SQL injections by using PDO's/MDB2's/DB's/ADOdb's prepare() and execute() methods.

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $artistid $_REQUEST['artistid'];
  6. $dom $query2xml->getXML(
  7.   array(
  8.     'data' => array(
  9.         ":$artistid"
  10.     ),
  11.     'query' => 'SELECT * FROM artist WHERE artistid = ?'
  12.   ),
  13.   array(
  14.     'rootTag' => 'favorite_artist',
  15.     'idColumn' => 'artistid',
  16.     'rowTag' => 'artist',
  17.     'elements' => array(
  18.       'name',
  19.       'birth_year',
  20.       'music_genre' => 'genre'
  21.     )
  22.   )
  23. );
  24. header('Content-Type: application/xml');
  25. $dom->formatOutput true;
  26. print $dom->saveXML();
  27. ?>
The resulting XML data is identical in both cases (given that artistid was submitted as 1):
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <favorite_artist>
  3.   <artist>
  4.     <name>Curtis Mayfield</name>
  5.     <birth_year>1920</birth_year>
  6.     <music_genre>Soul</music_genre>
  7.   </artist>
  8. </favorite_artist>

As stated above $sql can also be a boolean value of false. This will only be useful in scenarios where you want to combine the results of multiple unrelated queries into a single XML document. XML_Query2XML will deal with an $sql argument that has a value of false as if it executed a query that returned a single record with no colunns.

If you simpy wanted all the records of the table "album" and all the records of the table "artist" you could write code like this:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4.  
  5. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  6.  
  7. $dom $query2xml->getXML(
  8.     false,
  9.     array(
  10.         'idColumn' => false,
  11.         'rowTag' => '__tables',
  12.         'rootTag' => 'music_store',
  13.         'elements' => array(
  14.             'artists' => array(
  15.                 'rootTag' => 'artists',
  16.                 'rowTag' => 'artist',
  17.                 'idColumn' => 'artistid',
  18.                 'sql' => 'SELECT * FROM artist',
  19.                 'elements' => array(
  20.                     '*'
  21.                 )
  22.             ),
  23.             'albums' => array(
  24.                 'rootTag' => 'albums',
  25.                 'rowTag' => 'album',
  26.                 'idColumn' => 'albumid',
  27.                 'sql' => 'SELECT * FROM album',
  28.                 'elements' => array(
  29.                     '*'
  30.                 )
  31.             )
  32.         )
  33.     )
  34. );
  35.  
  36. header('Content-Type: application/xml');
  37. $dom->formatOutput true;
  38. print $dom->saveXML();
  39. ?>
In this case we actually are not interested in $sql at all; all we want is to get our $options['sql']s executed. Also note that we used '__tables' for $options['rowTag'] at the root level: this is because we don't have anything to loop over at the root level - remember using false for $sql is like using a query that returns a single record with no columns.

The resulting XML looks like this:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <music_store>
  3.   <artists>
  4.     <artist>
  5.       <artistid>1</artistid>
  6.       <name>Curtis Mayfield</name>
  7.       <birth_year>1920</birth_year>
  8.       <birth_place>Chicago</birth_place>
  9.       <genre>Soul</genre>
  10.     </artist>
  11.     <artist>
  12.       <artistid>2</artistid>
  13.       <name>Isaac Hayes</name>
  14.       <birth_year>1942</birth_year>
  15.       <birth_place>Tennessee</birth_place>
  16.       <genre>Soul</genre>
  17.     </artist>
  18.     <artist>
  19.       <artistid>3</artistid>
  20.       <name>Ray Charles</name>
  21.       <birth_year>1930</birth_year>
  22.       <birth_place>Mississippi</birth_place>
  23.       <genre>Country and Soul</genre>
  24.     </artist>
  25.   </artists>
  26.   <albums>
  27.     <album>
  28.       <albumid>1</albumid>
  29.       <artist_id>1</artist_id>
  30.       <title>New World Order</title>
  31.       <published_year>1990</published_year>
  32.       <comment>the best ever!</comment>
  33.     </album>
  34.     <album>
  35.       <albumid>2</albumid>
  36.       <artist_id>1</artist_id>
  37.       <title>Curtis</title>
  38.       <published_year>1970</published_year>
  39.       <comment>that man's got somthin' to say</comment>
  40.     </album>
  41.     <album>
  42.       <albumid>3</albumid>
  43.       <artist_id>2</artist_id>
  44.       <title>Shaft</title>
  45.       <published_year>1972</published_year>
  46.       <comment>he's the man</comment>
  47.     </album>
  48.   </albums>
  49. </music_store>

Want to dump not just two but all of your table? Have a look at Using dynamic $options to dump all data of your database.

$options['elements']

This option is an array that basically holds column names to include in the XML data as child elements. There are two types of element specifications:

Simple Element Specifications

These allow you to use an array to specify elements that have only two properties: a name and a value. The array values are used to specify the XML element values whereas the array keys are used to specify the XML element names. For all array elements that are defined without a key, the array values will be used for the XML element names. If no prefix is used (see below) the contents of the array values are interpreted as column names. The following example illustrates the most basic usage of a simple element specification:

  1. array(
  2.     'COLUMN1',
  3.     'COLUMN2'
  4. );
This might result in XML data like this:
  1. <COLUMN1>this was the contents of COLUMN1</COLUMN1>
  2. <COLUMN2>this was the contents of COLUMN2</COLUMN2>
If you do not want your XML elements named after your database columns you have to work with array keys (ELEMENT1 and ELEMENT2 in our example): while the element specification
  1. array(
  2.     'ELEMENT1' => 'COLUMN1',
  3.     'ELEMENT2' => 'COLUMN2'
  4. );
This would make the same data appear like this:
  1. <ELEMENT1>this was the contents of COLUMN1</ELEMENT1>
  2. <ELEMENT2>this was the contents of COLUMN2</ELEMENT2>

If you use both, the array key and the array value to specify an XML element, the array value can be of the following types:

  • COLUMN NAME (string): this is the default if not preceeded by ':' or '#'. If the column does not exist, an XML_Query2XML_ConfigException will be thrown.
  • STATIC TEXT with a : prefix (string): if the value is preceeded by a colon (':'), it is interpreted as static text.
  • CALLBACK FUNCTION with a # prefix (string): if the value is preceeded by a pound sign ('#'), it is interpreted as a callback function. You can use a regular function (e.g. '#myFunction()') or a static method (e.g. '#MyClass::myFunction()') - for how to use a non-static method, see the type COMMAND OBJECT. The current record will be passed to the callback function as an associative array. You can also pass additional string arguments to the callback function by specifing them within the opening and closing brace; e.g. '#Utils::limit(12)' will result in Util::limit() being called with the current record as the first and '12' as the second argument. If you do not want to pass additional arguments to the callback function, the opening and closing brace are optional. The callback function's return value will be converted to a string and used as the child text node if it is anything but an object or an array. If you do return an object or an array from a callback function it has to be an instance of DOMNode or an array of DOMNode instances. Please see Integrating other XML data sources for examples and further details. If an instances of any other class is returned, a XML_Query2XML_XMLException will be thrown.
  • COMMAND OBJECT (object): If you want to use a non-static method as a callback function, you can do so by specifying the value as an instance of a class that implements the XML_Query2XML_Callback interface. This implementation of the command pattern gives you all the flexibility. The disadvantage ist that you cannot use the XML UNSERIALIZATION prefix or the CONDITIONAL prefix. Note: you have to require_once 'XML/Query2XML/Callback.php' before using the XML_Query2XML_Callback interface. The return value of a COMMAND OBJECT's execute() method is treated exactly the same as the return value of a CALLBACK FUNCTION.
There are four more prefixes available that can be used in conjunction with all the prifixes described above:
  • XML UNSERIALIZATION prefix &: the ampersand (&) prefix allows you to automatically unserialize string data, i.e. transform a string into a DOMDocument. DOMDocument's loadXML() method will be used for this purpose. You can combine all three types with this prefix: '&COLUMN_NAME', '&#function()' or '&:<name>John</name>' will all work. You can even use the CONDITIONAL prefix which has to preceed all other prefixes. If the data cannot be unserialized i.e. DOMDocument::loadXML() returns false, a XML_Query2XML_XMLException will be thrown. Please see Integrating other XML data sources for examples and further details.
  • BASE64 ENCODING prefix ^: if the specification starts with a carrat sign ('^'), the element value will be passed to base64_encode(). The BASE64 ENCODING prefix can be used with all the prefixes described above (just put the BASE64 ENCODING prefix first): e.g. '^#', '^:' or '^COLUMN_NAME'.
  • CDATA SECTION prefix =: if the specification starts with an equal sign ('='), the element value will be enclosed in a CDATA section. A CDATA section starts with "<![CDATA[" and ends with "]]>". The CDATA SECTION prefix can be used with all the prefixes described above (just put the CDATA SECTION prefix first): e.g. '=#', '=:', '=COLUMN_NAME' or '=^'.
  • CONDITIONAL prefix ?: if the specification starts with a question mark ('?'), the whole element will be skipped if the value equals (==) an empty string. The CONDITIONAL prefix can be combined with all types described above: if you do this you have to write the CONDITIONAL prefix first e.g. '?#', '?:', '?&', '?=', '?^', or '?COLUMN_NAME'.
Note: for ovious reasons, the prefix cannot be combined with a COMMAND OBJECT.

Basically, the same syntax can be use for $options['value'], $options['attributes'], Complex Query Specification and $options['idColumn'] because the private method XML_Query2XML::_applyColumnStringToRecord() is used in all cases.

Let's start out with a very simple example. It will use the column name as the XML element name for the first two columns but the custom element name 'music_genre' for the column 'genre':

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.   "SELECT * FROM artist",
  7.   array(
  8.     'rootTag' => 'favorite_artists',
  9.     'idColumn' => 'artistid',
  10.     'rowTag' => 'artist',
  11.     'elements' => array(
  12.         'name',
  13.         'birth_year',
  14.         'music_genre' => 'genre'
  15.     )
  16.   )
  17. );
  18. header('Content-Type: application/xml');
  19. $dom->formatOutput true;
  20. print $dom->saveXML();
  21. ?>
This results in the following XML data:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <favorite_artists>
  3.   <artist>
  4.     <name>Curtis Mayfield</name>
  5.     <birth_year>1920</birth_year>
  6.     <music_genre>Soul</music_genre>
  7.   </artist>
  8.   <artist>
  9.     <name>Isaac Hayes</name>
  10.     <birth_year>1942</birth_year>
  11.     <music_genre>Soul</music_genre>
  12.   </artist>
  13.   <artist>
  14.     <name>Ray Charles</name>
  15.     <birth_year>1930</birth_year>
  16.     <music_genre>Country and Soul</music_genre>
  17.   </artist>
  18. </favorite_artists>

The following example demonstrates the usage of all different types:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'XML/Query2XML/Callback.php';
  4. require_once 'MDB2.php';
  5.  
  6. class Utils
  7. {
  8.     function trim($record$columnName)
  9.     {
  10.         return trim($record[$columnName]);
  11.     }
  12.     
  13.     function getPublishedYearCentury($record)
  14.     {
  15.         return floor($record['published_year']/100);
  16.     }
  17. }
  18.  
  19. class ToLowerCallback implements XML_Query2XML_Callback
  20. {
  21.     private $_columnName '';
  22.     
  23.     public function __construct($columnName)
  24.     {
  25.         $this->_columnName $columnName;
  26.     }
  27.     
  28.     public function execute(array $record)
  29.     {
  30.         return strtolower($record[$this->_columnName]);
  31.     }
  32. }
  33.  
  34. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  35. $dom $query2xml->getXML(
  36.   "SELECT
  37.     *
  38.    FROM
  39.     sale,
  40.     store,
  41.     album
  42.    WHERE
  43.     sale.store_id = store.storeid
  44.     AND
  45.     sale.album_id = album.albumid
  46.     AND
  47.     sale.timestamp < '2005-06-01'",
  48.   array(
  49.     'rootTag' => 'sales',
  50.     'idColumn' => 'saleid',
  51.     'rowTag' => 'sale',
  52.     'elements' => array(
  53.         'saleid',
  54.         'sale_timestamp' => 'timestamp',
  55.         'static' => ':some static text',
  56.         'now' => ':' time(),
  57.         'album_century' => '#Utils::getPublishedYearCentury()',
  58.         'album_title' => '?#Utils::trim(title)',
  59.         'album_comment' => new ToLowerCallback('comment'),
  60.         'storeid',
  61.         'store_building1' => '?&building_xmldata',
  62.         'store_building2' => '?=building_xmldata',
  63.         'store_building3' => '?^building_xmldata'
  64.     )
  65.   )
  66. );
  67.  
  68. header('Content-Type: application/xml');
  69. $dom->formatOutput true;
  70. print $dom->saveXML();
  71. ?>
Let's go through all simple element specifications, one by one:
  • 'saleid': this is as simple as it can get. The value of the column saleid will be used for an element named saleid.
  • 'sale_timestamp' => 'timestamp': here we want to place the value of the column timestamp in an element named sale_timestamp; we therefore use sale_timestamp as the array key.
  • 'static' => ':some static text': the STATIC TEXT (note the ":" prefix) "some static text" will be placed inside an element named static.
  • 'now' => ':' . time(): here the static text is computed at run time; however it will be the same for all "now" elements.
  • 'album_century' => '#Utils::getPublishedYearCentury()': here we use a CALLBACK FUNCTION with a "#" prefix; the return value of Utils::getPublishedYearCentury() is used as the XML element value. Note that the callback function will automatically be called with the current $record as the first argument.
  • 'album_title' => '?#Utils::trim(title)': we also use a CALLBACK FUNCTION with a "#" prefix, but this time we pass an additional string argument to our callback function by specifing it within the opening and closing brace. Also, we use the CONDITIONAL prefix ? which means that the album_title element will only appear in the generated XML data if Utils::trim() returned a non-empty string (to be precise a string that != "").
  • 'album_comment' => new ToLowerCallback('comment'): here we use a COMMAND OBJECT implementing the XML_Query2XML_Callback interface. This is the object oriented way to use callbacks! Note how we pass the column name to the callback class constructor, so that it's execute() method will now what column to work on.
  • 'storeid': plain an simple again
  • 'store_building1' => '?&building_xmldata': here we use the XML UNSERIALIZATION prefix "&" to transform the value of the building_xmldata column into a DOMDocument. Using the CONDITIONAL prefix ? means that store_building1 will only appear if building_xmldata is non-empty (!= "" to be precise).
  • 'store_building2' => '?=building_xmldata': CDATA SECTION prefix "=" is another way incorporate XML data; the contents of the column building_xmldata will be surrounded by "<![CDATA[" and "]]>". Using the CONDITIONAL prefix ? means that store_building2 will only appear if building_xmldata is non-empty (!= "" to be precise).
  • 'store_building3' => '?^building_xmldata': here we use the BASE64 ENCODING prefix "^" to first base64-encode the contents of the building_xmldata column. We again use the CONDITIONAL prefix "?".
The resulting XML data looks like this:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <sales>
  3.   <sale>
  4.     <saleid>1</saleid>
  5.     <sale_timestamp>2005-05-25 07:32:00</sale_timestamp>
  6.     <static>some static text</static>
  7.     <now>1187498966</now>
  8.     <album_century>19</album_century>
  9.     <album_title>New World Order</album_title>
  10.     <album_comment>the best ever!</album_comment>
  11.     <storeid>1</storeid>
  12.     <store_building1>
  13.       <building>
  14.         <floors>4</floors>
  15.         <elevators>2</elevators>
  16.         <square_meters>3200</square_meters>
  17.       </building>
  18.     </store_building1>
  19.     <store_building2>< ![CDATA[<building><floors>4</floors><elevators>2</elevators><square_meters>3200</square_meters></building>]] ></store_building2>
  20.     <store_building3>PGJ1aWxkaW5nPjxmbG9vcnM+NDwvZmxvb3JzPjxlbGV2YXRvcnM+MjwvZWxldmF0b3JzPjxzcXVhcmVfbWV0ZXJzPjMyMDA8L3NxdWFyZV9tZXRlcnM+PC9idWlsZGluZz4=</store_building3>
  21.   </sale>
  22.   <sale>
  23.     <saleid>11</saleid>
  24.     <sale_timestamp>2005-05-25 07:23:00</sale_timestamp>
  25.     <static>some static text</static>
  26.     <now>1187498966</now>
  27.     <album_century>19</album_century>
  28.     <album_title>Curtis</album_title>
  29.     <album_comment>that man's got somthin' to say</album_comment>
  30.     <storeid>2</storeid>
  31.     <store_building1>
  32.       <building>
  33.         <floors>2</floors>
  34.         <elevators>1</elevators>
  35.         <square_meters>400</square_meters>
  36.       </building>
  37.     </store_building1>
  38.     <store_building2>< ![CDATA[<building><floors>2</floors><elevators>1</elevators><square_meters>400</square_meters></building>]] ></store_building2>
  39.     <store_building3>PGJ1aWxkaW5nPjxmbG9vcnM+MjwvZmxvb3JzPjxlbGV2YXRvcnM+MTwvZWxldmF0b3JzPjxzcXVhcmVfbWV0ZXJzPjQwMDwvc3F1YXJlX21ldGVycz48L2J1aWxkaW5nPg==</store_building3>
  40.   </sale>
  41. </sales>
Note: due to a bug in phpDocumentor I had to cheat a little bit in the above XML; as you might have noticed there was a space between "<" and "![CDATA[".


Complex Element Specifications

A complex element specification consists of an array that can have all options that can be present on the root level plus $options['sql'] and $options['sql_options']. This allows for complete (and theoretically infinite) nesting. You will need to use it if the child element should have attributes or child elements.

The following example is like the first one in Simple Element Specifications with one difference: the XML element 'name' should have the attribute 'type' set to the static value 'full_name'. As attributes are not supported by simple elements specifications, we have to use a complex element specification for the element 'name':

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.   "SELECT * FROM artist",
  7.   array(
  8.     'rootTag' => 'favorite_artists',
  9.     'idColumn' => 'artistid',
  10.     'rowTag' => 'artist',
  11.     'elements' => array(
  12.         'name' => array(
  13.             'value' => 'name',
  14.             'attributes' => array(
  15.                 'type' => ':full_name'
  16.             )
  17.         ),
  18.         'birth_year',
  19.         'music_genre' => 'genre'
  20.     )
  21.   )
  22. );
  23. header('Content-Type: application/xml');
  24. $dom->formatOutput true;
  25. print $dom->saveXML();
  26. ?>
This results in the following XML data:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <favorite_artists>
  3.   <artist>
  4.     <name type="full_name">Curtis Mayfield</name>
  5.     <birth_year>1920</birth_year>
  6.     <music_genre>Soul</music_genre>
  7.   </artist>
  8.   <artist>
  9.     <name type="full_name">Isaac Hayes</name>
  10.     <birth_year>1942</birth_year>
  11.     <music_genre>Soul</music_genre>
  12.   </artist>
  13.   <artist>
  14.     <name type="full_name">Ray Charles</name>
  15.     <birth_year>1930</birth_year>
  16.     <music_genre>Country and Soul</music_genre>
  17.   </artist>
  18. </favorite_artists>

Here is another little example:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.     "SELECT * FROM artist LEFT JOIN album ON album.artist_id = artist.artistid",
  7.     array(
  8.         'rootTag' => 'music_library',
  9.         'rowTag' => 'artist',
  10.         'idColumn' => 'artistid',
  11.         'elements' => array(
  12.             'artistid',
  13.             'name',
  14.             'birth_year',
  15.             'birth_place',
  16.             'genre',
  17.             'albums' => array(
  18.               'rootTag' => 'albums',
  19.               'rowTag' => 'album',
  20.               'idColumn' => 'albumid',
  21.               'elements' => array('albumid''title''published_year')
  22.             )
  23.         )
  24.     )
  25. );
  26. header('Content-Type: application/xml');
  27. $dom->formatOutput true;
  28. print $dom->saveXML();
  29. ?>
This results in the following XML data:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <music_library>
  3.   <artist>
  4.     <artistid>1</artistid>
  5.     <name>Curtis Mayfield</name>
  6.     <birth_year>1920</birth_year>
  7.     <birth_place>Chicago</birth_place>
  8.     <genre>Soul</genre>
  9.     <albums>
  10.       <album>
  11.         <albumid>1</albumid>
  12.         <title>New World Order</title>
  13.         <published_year>1990</published_year>
  14.       </album>
  15.       <album>
  16.         <albumid>2</albumid>
  17.         <title>Curtis</title>
  18.         <published_year>1970</published_year>
  19.       </album>
  20.     </albums>
  21.   </artist>
  22.   <artist>
  23.     <artistid>2</artistid>
  24.     <name>Isaac Hayes</name>
  25.     <birth_year>1942</birth_year>
  26.     <birth_place>Tennessee</birth_place>
  27.     <genre>Soul</genre>
  28.     <albums>
  29.       <album>
  30.         <albumid>3</albumid>
  31.         <title>Shaft</title>
  32.         <published_year>1972</published_year>
  33.       </album>
  34.     </albums>
  35.   </artist>
  36.   <artist>
  37.     <artistid>3</artistid>
  38.     <name>Ray Charles</name>
  39.     <birth_year>1930</birth_year>
  40.     <birth_place>Mississippi</birth_place>
  41.     <genre>Country and Soul</genre>
  42.     <albums />
  43.   </artist>
  44. </music_library>

As we want for every artist only a single tag we need to identify each artist by the primary key of the table artist. Note that there is a second record for Curtis Mayfield (related to the album Curtis), but we don't want something like

  1. <artist>
  2.   <name>Curtis Mayfield</name>
  3.   <album>
  4.     <name>New World Order</name>
  5.   </album>
  6. </artist>
  7. <artist>
  8.   <name>Curtis Mayfield</name>
  9.   <album>
  10.     <name>Curits</name>
  11.   </album>
  12. </artist>
but rather
  1. <artist>
  2.   <name>Curtis Mayfield</name>
  3.   <albums>
  4.     <album>
  5.      <name>New World Order</name>
  6.     </album>
  7.     <albums>
  8.      <name>Curtis</name>
  9.     </albums>
  10.   </albums>
  11. </artist>
This is achieved by telling XML_Query2XML which entity to focus on (on this level): the artist, as it is identified by the artist table's primary key. Once XML_Query2XML get's to the second Curtis Mayfield record, it can tell by the artistid 1 that an XML element was already created for this artist.

For a one more example and a detailed explanation of complex child elements that have child elements themselves, see Case 02: LEFT OUTER JOIN. For an advanced example, see Case 05: three LEFT OUTER JOINs.


Using the Asterisk Shortcut

The asterisk shortcut only works with Simple Element Specifications (and Simple Attribute Specifications).

In some scenarios you will just want to use all columns found in the result set for Simple Element Specifications. This is where the asterisk shortcut can come in very handy. An element specification that contains an asterisk (an "asterisk element specification") will be duplicated for each column present in the result set ($record). The simplest way of using the asterisk shortcut is this:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.   "SELECT * FROM artist",
  7.   array(
  8.     'rootTag' => 'favorite_artists',
  9.     'idColumn' => 'artistid',
  10.     'rowTag' => 'artist',
  11.     'elements' => array(
  12.         '*'
  13.     )
  14.   )
  15. );
  16. header('Content-Type: application/xml');
  17. $dom->formatOutput true;
  18. print $dom->saveXML();
  19. ?>
As the result set contains the column artistid, name, birth_year, birth_place and genre the XML data will look like this:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <favorite_artists>
  3.   <artist>
  4.     <artistid>1</artistid>
  5.     <name>Curtis Mayfield</name>
  6.     <birth_year>1920</birth_year>
  7.     <birth_place>Chicago</birth_place>
  8.     <genre>Soul</genre>
  9.   </artist>
  10.   <artist>
  11.     <artistid>2</artistid>
  12.     <name>Isaac Hayes</name>
  13.     <birth_year>1942</birth_year>
  14.     <birth_place>Tennessee</birth_place>
  15.     <genre>Soul</genre>
  16.   </artist>
  17.   <artist>
  18.     <artistid>3</artistid>
  19.     <name>Ray Charles</name>
  20.     <birth_year>1930</birth_year>
  21.     <birth_place>Mississippi</birth_place>
  22.     <genre>Country and Soul</genre>
  23.   </artist>
  24. </favorite_artists>
This is because internally, the array
  1. 'elements' => array(
  2.   '*'
  3. )
is expanded to
  1. 'elements' => array(
  2.   'artistid',
  3.   'name',
  4.   'birth_year',
  5.   'birth_place',
  6.   'genre'
  7. )

Think of the asterisk as a variable that will get replaced with each column name found in the result set:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.   "SELECT * FROM artist",
  7.   array(
  8.     'rootTag' => 'favorite_artists',
  9.     'idColumn' => 'artistid',
  10.     'rowTag' => 'artist',
  11.     'elements' => array(
  12.         'TAG_*' => '#padWithHyphens(*)'
  13.     )
  14.   )
  15. );
  16. header('Content-Type: application/xml');
  17. $dom->formatOutput true;
  18. print $dom->saveXML();
  19.  
  20. function padWithHyphens($record$columnName)
  21. {
  22.     return '--' $record[$columnName'--';
  23. }
  24. ?>
The above code would result in the following data:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <favorite_artists>
  3.   <artist>
  4.     <TAG_artistid>--1--</TAG_artistid>
  5.     <TAG_name>--Curtis Mayfield--</TAG_name>
  6.     <TAG_birth_year>--1920--</TAG_birth_year>
  7.     <TAG_birth_place>--Chicago--</TAG_birth_place>
  8.     <TAG_genre>--Soul--</TAG_genre>
  9.   </artist>
  10.   <artist>
  11.     <TAG_artistid>--2--</TAG_artistid>
  12.     <TAG_name>--Isaac Hayes--</TAG_name>
  13.     <TAG_birth_year>--1942--</TAG_birth_year>
  14.     <TAG_birth_place>--Tennessee--</TAG_birth_place>
  15.     <TAG_genre>--Soul--</TAG_genre>
  16.   </artist>
  17.   <artist>
  18.     <TAG_artistid>--3--</TAG_artistid>
  19.     <TAG_name>--Ray Charles--</TAG_name>
  20.     <TAG_birth_year>--1930--</TAG_birth_year>
  21.     <TAG_birth_place>--Mississippi--</TAG_birth_place>
  22.     <TAG_genre>--Country and Soul--</TAG_genre>
  23.   </artist>
  24. </favorite_artists>

You can also combine a simple element specification containing an asterisk shortcut with other (simple and complex) element specifications. The additional element specifications will be treated as an exception to the general rule set up by the asterisk element specification. The following code will produce a tag for each column in the result set containing the column's value. The only exeption is the column "genre" which we want to be different: the value should be all uppercase:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.   "SELECT * FROM artist",
  7.   array(
  8.     'rootTag' => 'favorite_artists',
  9.     'idColumn' => 'artistid',
  10.     'rowTag' => 'artist',
  11.     'elements' => array(
  12.         '*' => '*',
  13.         'genre' => '#genre2uppercase()'
  14.     )
  15.   )
  16. );
  17. header('Content-Type: application/xml');
  18. $dom->formatOutput true;
  19. print $dom->saveXML();
  20.  
  21. function genre2uppercase($record)
  22. {
  23.     return strtoupper($record['genre']);
  24. }
  25. ?>
The resulting XML data looks like this:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <favorite_artists>
  3.   <artist>
  4.     <artistid>1</artistid>
  5.     <name>Curtis Mayfield</name>
  6.     <birth_year>1920</birth_year>
  7.     <birth_place>Chicago</birth_place>
  8.     <genre>SOUL</genre>
  9.   </artist>
  10.   <artist>
  11.     <artistid>2</artistid>
  12.     <name>Isaac Hayes</name>
  13.     <birth_year>1942</birth_year>
  14.     <birth_place>Tennessee</birth_place>
  15.     <genre>SOUL</genre>
  16.   </artist>
  17.   <artist>
  18.     <artistid>3</artistid>
  19.     <name>Ray Charles</name>
  20.     <birth_year>1930</birth_year>
  21.     <birth_place>Mississippi</birth_place>
  22.     <genre>COUNTRY AND SOUL</genre>
  23.   </artist>
  24. </favorite_artists>
This is because internally, the array
  1. 'elements' => array(
  2.   '*' => '*',
  3.   'genre' => '#genre2uppercase()'
  4. )
is expanded to
  1. 'elements' => array(
  2.   'artistid',
  3.   'name',
  4.   'birth_year',
  5.   'birth_place',
  6.   'genre' => '#genre2uppercase()'
  7. )
Please keep in mind that this also applies when combining an asterisk element specification with a complex element specification. That's why the following code would produce exactly the same XML data:
  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.   "SELECT * FROM artist",
  7.   array(
  8.     'rootTag' => 'favorite_artists',
  9.     'idColumn' => 'artistid',
  10.     'rowTag' => 'artist',
  11.     'elements' => array(
  12.         '*' => '*',
  13.         'genre' => array(
  14.             'value' => '#genre2uppercase()'
  15.         )
  16.     )
  17.   )
  18. );
  19. header('Content-Type: application/xml');
  20. $dom->formatOutput true;
  21. print $dom->saveXML();
  22.  
  23. function genre2uppercase($record)
  24. {
  25.     return strtoupper($record['genre']);
  26. }
  27. ?>

If we wanted to include all columns in the XML output except "genre" we could use a little trick:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.   "SELECT * FROM artist",
  7.   array(
  8.     'rootTag' => 'favorite_artists',
  9.     'idColumn' => 'artistid',
  10.     'rowTag' => 'artist',
  11.     'elements' => array(
  12.         '*' => '*',
  13.         'genre' => '?:'
  14.     )
  15.   )
  16. );
  17. header('Content-Type: application/xml');
  18. $dom->formatOutput true;
  19. print $dom->saveXML();
  20. ?>
In the resulting XML data the column "genre" is missing because we used the CONDITIONAL prefix '?' in combination with a static empty text:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <favorite_artists>
  3.   <artist>
  4.     <artistid>1</artistid>
  5.     <name>Curtis Mayfield</name>
  6.     <birth_year>1920</birth_year>
  7.     <birth_place>Chicago</birth_place>
  8.   </artist>
  9.   <artist>
  10.     <artistid>2</artistid>
  11.     <name>Isaac Hayes</name>
  12.     <birth_year>1942</birth_year>
  13.     <birth_place>Tennessee</birth_place>
  14.   </artist>
  15.   <artist>
  16.     <artistid>3</artistid>
  17.     <name>Ray Charles</name>
  18.     <birth_year>1930</birth_year>
  19.     <birth_place>Mississippi</birth_place>
  20.   </artist>
  21. </favorite_artists>
The exact same result could of course also be achieved using the "condition" option:
  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.   "SELECT * FROM artist",
  7.   array(
  8.     'rootTag' => 'favorite_artists',
  9.     'idColumn' => 'artistid',
  10.     'rowTag' => 'artist',
  11.     'elements' => array(
  12.         '*' => '*',
  13.         'genre' => array(
  14.             'condition' => '#returnFalse()'
  15.             //this would also work: 'condition' => ':'
  16.         )
  17.     )
  18.   )
  19. );
  20. header('Content-Type: application/xml');
  21. $dom->formatOutput true;
  22. print $dom->saveXML();
  23.  
  24. function returnFalse()
  25. {
  26.     return false;
  27. }
  28. ?>

Another example of how to use the asterisk shortcut can be found in Case 07: Case 03 with Asterisk Shortcuts.

One final note on the asterisk shortcut: if you explicitly specify a tag name (an array element key) it has to contain an asterisk. The following code would cause a XML_Query2XML_ConfigException to be thrown:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.   "SELECT * FROM artist",
  7.   array(
  8.     'rootTag' => 'favorite_artists',
  9.     'idColumn' => 'artistid',
  10.     'rowTag' => 'artist',
  11.     'elements' => array(
  12.         'tag' => '*'
  13.     )
  14.   )
  15. );
  16. header('Content-Type: application/xml');
  17. $dom->formatOutput true;
  18. print $dom->saveXML();
  19. ?>
This is because expanding
  1. 'elements' => array(
  2.   'tag' => '*'
  3. )
to
  1. 'elements' => array(
  2.   'tag' => 'artistid',
  3.   'tag' => 'name',
  4.   'tag' => 'birth_year',
  5.   'tag' => 'birth_place',
  6.   'tag' => 'genre'
  7. )
just makes no sense and therfore "*" is treated as a regular column name - which does not exist in this case! The exception's message would read: [elements]: The column "*" used in the option "tag" does not exist in the result set.


$options['idColumn']

In most cases this will be the name of the column by which a record is identified as unique, aka the primary key. This is especially important within a Complex Element Specification. See there for an example. This option is obligatory at the root level! The idColumn specification can be of the following types:

  • COLUMN NAME: this is the default if not preceeded by ':' or '#'. If the column does not exist, an XML_Query2XML_ConfigException will be thrown. The current record (not the one of the parent level) will be used.
  • STATIC TEXT with a : prefix: if the value is preceeded by a colon (':'), it is interpreted as static text.
  • CALLBACK FUNCTION with a # prefix: if the value is preceeded by a pound sign ('#'), it is interpreted as a callback function. You can use a regular function (e.g. '#myFunction()') or a static method (e.g. '#MyClass::myFunction()') - for how to use a non-static method, see the type COMMAND OBJECT. The current record will be passed to the callback function as an associative array. You can also pass additional string arguments to the callback function by specifing them within the opening and closing brace; e.g. '#Utils::limit(12)' will result in Util::limit() being called with the current record as the first and '12' as the second argument. If you do not want to pass additional arguments to the callback function, the opening and closing brace are optional.
  • COMMAND OBJECT (object): If you want to use a non-static method as a callback function, you can do so by specifying the value as an instance of a class that implements the XML_Query2XML_Callback interface. This implementation of the command pattern gives you all the flexibility. Note: you have to require_once 'XML/Query2XML/Callback.php' before using the XML_Query2XML_Callback interface. The return value of a COMMAND OBJECT's execute() method is treated exactly the same as the return value of a CALLBACK FUNCTION.
  • FALSE (boolean): Only use this if you don't have a primary key (which is a very bad idea) or you have a very simple tasks at hand like retrieving all records from a table. Using the value FALSE will make XML_Query2XML treat every record as unique. WARNING: it is considered very bad practice to use a value of FALSE if you have a way to specify your primar key. This is because your code might change over time and having your primary key specified will just make your more stable. For a legitimate use of the value FALSE for the option idColumn, please see Using dynamic $options to dump all data of your database.
The same syntax (with the additional '?' prefix but without the boolean value FALSE) can be use for $options['value'], Simple Attribute Specifications, Complex Query Specification and Simple Element Specifications because the private method XML_Query2XML::_applyColumnStringToRecord() is used in all cases.

For example and further discussion of $options['idColumn'] please see Case 02: LEFT OUTER JOIN.

Handling Multi-Column Primary Keys

Sometimes your primary key will consist of multiple columns. For example, this might be the case when you implement a many-to-many relationship using an intersection table.

But as you know by now, $options['idColumn'] has to evaluate to one unique ID for every record. Depending on the type of the primary key columns you will want to choose a different strategy to compute that unique ID for every record. To begin with, you have to choose whether you want to compute that unique ID within the database or using PHP. To do it within the database you will have to define an alias using the "... AS alias_name" syntax. Using PHP you have to use a callback function to generate the ID. When generating the ID, you again have different options.

If your primary key columns are of a numeric type, you can

  • Concatenate your primary key columns using a separator. In the database you would use something like "CONCAT(column1, '_', column2) AS id" and when implemented in PHP you would write a callback that returns something like "$record['column1'] . '_' . $record['column2']".
  • If Leftshift the first column by the number of bits the second column consumes and OR the leftshifted first column with the second column. E.g. if you had two columns defined as TINYINT UNSIGNED (0 to 255, i.e. 8 bits), you could generate the ID by
    1. $id ($column1 << 8$column2;
    Think of like this:
    $column1 (set to 255):           00000000000000000000000011111111
    $column2 left-shifted by 8 bits: 00000000000000001111111100000000
    
    now we OR the left-shifted $column1 with $column2 (both were
    originally set to the maximum of 255):
    $column1 (left-shifted 255): 00000000000000001111111100000000
    $column2 (set to 255):       00000000000000000000000011111111
    -------------------------------------------------------------
    result:                      00000000000000001111111111111111
    As you can see two different combinations of $column1 and $column2
    will always result in a different ID. This is because the left-shifted
    $column1 does not intersect with $column2.
           
    WARNING: an integer in PHP has 32 bits - the tecnique described above therefore only works if the sum of the bits consumed by your primary key columns is less than or equal to 32 (i.e. two 16 bit numbers, four 8 bit numbers or two 8 bit numbers and one 16 bit number).
  • Use the generateIdFromMultiKeyPK() method described below.

If your primary key columns are of a character type (e.g. CHAR, VARCHAR) you have to come up with something else. Before you read on, I strongly urge you to reconsider your choice for the primary key (does it really meet the requirements of minimality and stability, i.e. is immutable?). SECURITY WARNING: Do not simply concatenate your character type columns (with or without a separator). The following example shows why:

record1: column1='a_b' column2='c'
record2: column1='a'   column2='b_c'
When using the separator '_' both records would have an ID of 'a_b_c'.
     
A malicious attacker could use your separator within one of the column values to force ID collisions, which potentially lead to an exploitable security vulnerability. Great care should therefore be taken when choosing a separator - and relying on its confidentiality is not a good strategy. What you might do is to use a separator that is longer than the maximum character length of your primary key columns. But this only makes sense if that maximum is rather low. For example, if you have two CHAR(2) columns, it is reasonable to use the separator '---' which is three characters long.

Another thing one might think of is to use a hash function like sha1() or md5(). But that's not really an option as it would really kill the performance of your application.

The most bullet proof solution to the problem of generating a unique ID from two character type columns is to use a callback function that works with an array. The following function can be used as a callback whenever you need to generate an ID from two character type columns.

  1. <?php
  2. /**Returns a unique ID base on the values stored in
  3. * $record[$columnName1] and $record[$columnName2].
  4. * @param array $record An associative array.
  5. * @param string $columnName1 The name of the first column.
  6. * @param string $columnName2 The name of the second column.
  7. * @return int The ID.
  8. */
  9. function generateIdFromMultiKeyPK($record$columnName1$columnName2)
  10. {
  11.     static $ids array();
  12.     static $idCounter 0;
  13.     
  14.     $column1 $record[$columnName1];
  15.     $column2 $record[$columnName2];
  16.     if (!isset($ids[$column1])) {
  17.         $ids[$column1array();
  18.     }
  19.     if (!isset($ids[$column1][$column2])) {
  20.         $ids[$column1][$column2$idCounter++;
  21.     }
  22.     return $ids[$column1][$column2];
  23. }
  24. ?>

All you have to do is to specify $options['idColumn'] as:

'#generateIdFromMultiKeyPK(name_of_column1, name_of_column2)'
     
Remember: $record is automatically passed as the first argument to the callback function.


$options['attributes']

This option is an array that holds columns to include in the XML data as attributes. Simple and complex attribute specifications are supported.

If you want to add attributes to the root element (i.e. the first child of the DOMDocument instance returned by getXML()), please see Modifying the returned DOMDocument instance.

Simple Attribute Specifications

It works like Simple Element Specifications: the column names are the array values. By default the column's value will be put into an attribute named after the column. If you're unhappy with the default you can specify an other attribute name by using it as the array key. As documented for Simple Element Specifications the prefixes "?", "#", "^" and ":" or a COMMAND OBJECT can be used. Only the UNSERIALIZATION prefix & and the CDATA SECTION prefix ^ which are valid for a Simple Element Specification cannot be used for a Simple Attribute Specification.

The follwing example will use the column name as the attribute name for the first two columns but the custom attribute name 'music_genre' for the column 'genre':

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.   "SELECT * FROM artist",
  7.   array(
  8.     'rootTag' => 'favorite_artists',
  9.     'idColumn' => 'artistid',
  10.     'rowTag' => 'artist',
  11.     'elements' => array(),
  12.     'attributes' => array(
  13.       'name',
  14.       'birth_year',
  15.       'music_genre' => 'genre'
  16.     )
  17.   )
  18. );
  19. header('Content-Type: application/xml');
  20. $dom->formatOutput true;
  21. print $dom->saveXML();
  22. ?>
This results in the following XML data:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <favorite_artists>
  3.   <artist name="Curtis Mayfield" birth_year="1920" music_genre="Soul"/>
  4.   <artist name="Isaac Hayes" birth_year="1942" music_genre="Soul"/>
  5.   <artist name="Ray Charles" birth_year="1930" music_genre="Country and Soul"/>
  6. </favorite_artists>


Complex Attribute Specifications

A complex attribute specification consists of an array that must contain

  • $options['value']: the attribute's value (note: you cannot use the UNSERIALIZATION prefix & or the the CDATA SECTION prefix ^ for an attribute specification)
and optionally can contain The array key used to store the complex attribute specification is always used as the attribute's name. Unlike Complex Element Specifications complex attribute specifications cannot be nested for obvious reasons. Complex attribute specifications should only be used for the following reasons:
  • the attribute is only to be included under a condition that cannot be expressed using the '?' prefix within a simple attribute specification
  • additional data is needed from the database
In all other cases Simple Attribute Specifications should be used as they will make your code run faster.

To add a "bornBefore1940" attribute only to those artists that were born before 1940 we could write:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.   "SELECT * FROM artist",
  7.   array(
  8.     'rootTag' => 'favorite_artists',
  9.     'idColumn' => 'artistid',
  10.     'rowTag' => 'artist',
  11.     'attributes' => array(
  12.       'name',
  13.       'birth_year',
  14.       'bornBefore1940' => array(
  15.         'value' => ':true',
  16.         'condition' => '#lessThan(birth_year, 1940)'
  17.       )
  18.     )
  19.   )
  20. );
  21. header('Content-Type: application/xml');
  22. $dom->formatOutput true;
  23. print $dom->saveXML();
  24.  
  25. function lessThan($record$columnName$num)
  26. {
  27.     return $record[$columnName$num;
  28. }
  29. ?>
This results in the following XML data:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <favorite_artists>
  3.   <artist birth_year="1920" bornBefore1940="true" name="Curtis Mayfield" />
  4.   <artist birth_year="1942" name="Isaac Hayes" />
  5.   <artist birth_year="1930" bornBefore1940="true" name="Ray Charles" />
  6. </favorite_artists>

In the next example we want a "firstAlbumTitle" attribute for each artist. For the purpose of the example we will not use a single left outer join but a complex attribute specification with the "sql" option. As retrieving more than one record for a single attribute makes no sense $options['sql_options']['single_record'] is always automatically set to true when fetching records for attributes.

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.   "SELECT * FROM artist",
  7.   array(
  8.     'rootTag' => 'favorite_artists',
  9.     'idColumn' => 'artistid',
  10.     'rowTag' => 'artist',
  11.     'attributes' => array(
  12.       'name',
  13.       'birth_year',
  14.       'firstAlbumTitle' => array(
  15.         'value' => 'title',
  16.         'sql' => array(
  17.           'data' => array(
  18.             'artistid'
  19.           ),
  20.           'query' => "SELECT * FROM album WHERE artist_id = ? ORDER BY published_year"
  21.         )
  22.       )
  23.     )
  24.   )
  25. );
  26. header('Content-Type: application/xml');
  27. $dom->formatOutput true;
  28. print $dom->saveXML();
  29. ?>
This results in the following XML data:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <favorite_artists>
  3.   <artist birth_year="1920" firstAlbumTitle="Curtis" name="Curtis Mayfield" />
  4.   <artist birth_year="1942" firstAlbumTitle="Shaft" name="Isaac Hayes" />
  5.   <artist birth_year="1930" name="Ray Charles" />
  6. </favorite_artists>
As you can see, the firstAlbumTitle attribute is missing for Ray Charles. This is because he does not have any albums in our test database and processing the "value" option without any records just makes no sense.

In the last example I'd like to demonstrate the use of $options['sql_options'] within a complex attribute specification. As stated before, $options['sql_options']['single_record'] is always automatically set to true - no matter what you assign to it. This time, we want a "firstAlbum" attribute that has a value of "TITLE (GENRE)" - remember that "genre" is a colum of the artist table while "title" is a column of the album table.

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.   "SELECT * FROM artist",
  7.   array(
  8.     'rootTag' => 'favorite_artists',
  9.     'idColumn' => 'artistid',
  10.     'rowTag' => 'artist',
  11.     'attributes' => array(
  12.       'name',
  13.       'birth_year',
  14.       'firstAlbum' => array(
  15.         'value' => '#combineTitleAndGenre()',
  16.         'sql' => array(
  17.           'data' => array(
  18.             'artistid'
  19.           ),
  20.           'query' => "SELECT * FROM album WHERE artist_id = ? ORDER BY published_year"
  21.         ),
  22.         'sql_options' => array(
  23.           'merge_selective' => array('genre')
  24.         )
  25.       )
  26.     )
  27.   )
  28. );
  29. header('Content-Type: application/xml');
  30. $dom->formatOutput true;
  31. print $dom->saveXML();
  32.  
  33. function combineTitleAndGenre($record)
  34. {
  35.     return $record['title'' (' $record['genre'')';
  36. }
  37. ?>
This results in the following XML data:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <favorite_artists>
  3.   <artist name="Curtis Mayfield" birth_year="1920" firstAlbum="Curtis (Soul)"/>
  4.   <artist name="Isaac Hayes" birth_year="1942" firstAlbum="Shaft (Soul)"/>
  5.   <artist name="Ray Charles" birth_year="1930"/>
  6. </favorite_artists>


Using the Asterisk Shortcut

The asterisk shortcut only works with Simple Attribute Specifications (and Simple Element Specifications).

Everything said about Using the Asterisk Shortcut with simple element specifications applies here to! The simplest example of using the asterisk shortcut with the attributes option is as follows:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.   "SELECT * FROM artist",
  7.   array(
  8.     'rootTag' => 'favorite_artists',
  9.     'idColumn' => 'artistid',
  10.     'rowTag' => 'artist',
  11.     'attributes' => array(
  12.       '*'
  13.     )
  14.   )
  15. );
  16. header('Content-Type: application/xml');
  17. $dom->formatOutput true;
  18. print $dom->saveXML();
  19. ?>
This produces this XML data:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <favorite_artists>
  3.   <artist artistid="1" birth_place="Chicago" birth_year="1920" genre="Soul" name="Curtis Mayfield" />
  4.   <artist artistid="2" birth_place="Tennessee" birth_year="1942" genre="Soul" name="Isaac Hayes" />
  5.   <artist artistid="3" birth_place="Mississippi" birth_year="1930" genre="Country and Soul" name="Ray Charles" />
  6. </favorite_artists>


$options['rowTag']

The name of the tag that encloses each record. The default is 'row'.

Here goes an example of 'rowTag' being used at the root level:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.   "SELECT * FROM artist",
  7.   array(
  8.     'rootTag' => 'favorite_artists',
  9.     'idColumn' => 'artistid',
  10.     'rowTag' => 'artist',
  11.     'elements' => array(
  12.         'name',
  13.         'birth_year',
  14.         'genre'
  15.     )
  16.   )
  17. );
  18. header('Content-Type: application/xml');
  19. $dom->formatOutput true;
  20. print $dom->saveXML();
  21. ?>
'rowTag' was set to 'artist' therefore the resulting XML data is:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <favorite_artists>
  3.   <artist>
  4.     <name>Curtis Mayfield</name>
  5.     <birth_year>1920</birth_year>
  6.     <genre>Soul</genre>
  7.   </artist>
  8.   <artist>
  9.     <name>Isaac Hayes</name>
  10.     <birth_year>1942</birth_year>
  11.     <genre>Soul</genre>
  12.   </artist>
  13.   <artist>
  14.     <name>Ray Charles</name>
  15.     <birth_year>1930</birth_year>
  16.     <genre>Country and Soul</genre>
  17.   </artist>
  18. </favorite_artists>

Now let's have a look at a more advanced example:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.     "SELECT
  7.         *
  8.      FROM
  9.         artist
  10.         LEFT JOIN album ON album.artist_id = artist.artistid",
  11.     array(
  12.         'rootTag' => 'music_library',
  13.         'rowTag' => 'artist',
  14.         'idColumn' => 'artistid',
  15.         'elements' => array(
  16.             'artistid',
  17.             'name',
  18.             'birth_year',
  19.             'birth_place',
  20.             'genre',
  21.             'albums' => array(
  22.                 'rootTag' => 'albums',
  23.                 'rowTag' => 'album',
  24.                 'idColumn' => 'albumid',
  25.                 'elements' => array(
  26.                     'albumid',
  27.                     'title',
  28.                     'published_year',
  29.                     'comment'
  30.                 )
  31.             )
  32.         )
  33.     )
  34. );
  35.  
  36. header('Content-Type: application/xml');
  37. $dom->formatOutput true;
  38. print $dom->saveXML();
  39. ?>
Here 'rowTag' on the root level is set to 'artist' while ['elements']['albums']['rowTag'] is set to 'album'. This example is taken from Case 02: LEFT OUTER JOIN, so please see there for the resulting XML data and further discussion.

In some situations, 'rowTag' can be omitted all together:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.   "SELECT * FROM artist",
  7.   array(
  8.     'rootTag' => 'favorite_artists',
  9.     'idColumn' => 'artistid',
  10.     'rowTag' => 'artist',
  11.     'elements' => array(
  12.         'name' => array(
  13.             'value' => 'name',
  14.             'attributes' => array(
  15.                 'type' => ':full_name'
  16.             )
  17.         ),
  18.         'birth_year',
  19.         'music_genre' => 'genre'
  20.     )
  21.   )
  22. );
  23. header('Content-Type: application/xml');
  24. $dom->formatOutput true;
  25. print $dom->saveXML();
  26. ?>
Here the complex element definition ['elements']['name'] has no 'rowTag' option. This is alright because the specification's array key ('name' in this case) is used by default.

$options['dynamicRowTag']

Use this option if you want the name of an XML element determined at run time (e.g. you want to pull the XML element name from the database). Note: if this option is present, $options['rowTag'] will be ignored.

What you can assign to $options['dynamicRowTag'] is very similar as what you can use for $options['value'] or a Simple Element Specification. $options['dynamicRowTag'] can be of the following types:

  • COLUMN NAME: this is the default if not preceeded by ':' or '#'. If the column does not exist, an XML_Query2XML_ConfigException will be thrown.
  • STATIC TEXT with a : prefix: if the value is preceeded by a colon (':'), it is interpreted as static text.
  • CALLBACK FUNCTION with a # prefix: if the value is preceeded by a pound sign ('#'), it is interpreted as a callback function. You can use a regular function (e.g. '#myFunction()') or a static method (e.g. '#MyClass::myFunction()') - for how to use a non-static method, see the type COMMAND OBJECT. The current record will be passed to the callback function as an associative array. You can also pass additional string arguments to the callback function by specifing them within the opening and closing brace; e.g. '#Utils::limit(12)' will result in Util::limit() being called with the current record as the first and '12' as the second argument. If you do not want to pass additional arguments to the callback function, the opening and closing brace are optional. The callback function's return value obviously has to be a string that is a valid XML element name.
  • COMMAND OBJECT (object): If you want to use a non-static method as a callback function, you can do so by specifying the value as an instance of a class that implements the XML_Query2XML_Callback interface. This implementation of the command pattern gives you all the flexibility. The disadvantage ist that you cannot use the XML UNSERIALIZATION prefix or the CONDITIONAL prefix. Note: you have to require_once 'XML/Query2XML/Callback.php' before using the XML_Query2XML_Callback interface. The return value of a COMMAND OBJECT's execute() method is treated exactly the same as the return value of a CALLBACK FUNCTION.

Let's have a look at a straightforward example: we want our customer's email addresses inside a tag named after the customer's first name, e.g. <John>john.doe@example.com</John>:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.   "SELECT * FROM customer",
  7.   array(
  8.     'rootTag' => 'customers',
  9.     'idColumn' => 'customerid',
  10.     'rowTag' => 'customer',
  11.     'elements' => array(
  12.         'customerid',
  13.         'name_and_email' => array(
  14.             'dynamicRowTag' => 'first_name',
  15.             'value' => 'email'
  16.         )
  17.     )
  18.   )
  19. );
  20. header('Content-Type: application/xml');
  21. $dom->formatOutput true;
  22. print $dom->saveXML();
  23. ?>
The resulting XML looks like this:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <customers>
  3.   <customer>
  4.     <customerid>1</customerid>
  5.     <Jane>jane.doe@example.com</Jane>
  6.   </customer>
  7.   <customer>
  8.     <customerid>2</customerid>
  9.     <John>john.doe@example.com</John>
  10.   </customer>
  11.   <customer>
  12.     <customerid>3</customerid>
  13.     <Susan>susan.green@example.com</Susan>
  14.   </customer>
  15.   <customer>
  16.     <customerid>4</customerid>
  17.     <Victoria>victory.alt@example.com</Victoria>
  18.   </customer>
  19.   <customer>
  20.     <customerid>5</customerid>
  21.     <Will>will.wippy@example.com</Will>
  22.   </customer>
  23.   <customer>
  24.     <customerid>6</customerid>
  25.     <Tim>tim.raw@example.com</Tim>
  26.   </customer>
  27.   <customer>
  28.     <customerid>7</customerid>
  29.     <Nick>nick.fallow@example.com</Nick>
  30.   </customer>
  31.   <customer>
  32.     <customerid>8</customerid>
  33.     <Ed>ed.burton@example.com</Ed>
  34.   </customer>
  35.   <customer>
  36.     <customerid>9</customerid>
  37.     <Jack>jack.woo@example.com</Jack>
  38.   </customer>
  39.   <customer>
  40.     <customerid>10</customerid>
  41.     <Maria>maria.gonzales@example.com</Maria>
  42.   </customer>
  43. </customers>

$options['rootTag']

The name of the root tag that encloses all other tags. On the root level, the default is 'root'. On all other levels omitting the rootTag option means that the row tags will not be enclosed by a root tag but will directly be placed inside the parent tag.

Here goes an example of 'rootTag' being used at the root level:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.   "SELECT * FROM artist",
  7.   array(
  8.     'rootTag' => 'favorite_artists',
  9.     'idColumn' => 'artistid',
  10.     'rowTag' => 'artist',
  11.     'elements' => array(
  12.         'name',
  13.         'birth_year',
  14.         'genre'
  15.     )
  16.   )
  17. );
  18. header('Content-Type: application/xml');
  19. $dom->formatOutput true;
  20. print $dom->saveXML();
  21. ?>
'rootTag' was set to 'favorite_artists'. The resulting XML data therefore is:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <favorite_artists>
  3. <artist>
  4.   <name>Curtis Mayfield</name>
  5.   <birth_year>1920</birth_year>
  6.   <genre>Soul</genre>
  7. </artist>
  8. <artist>
  9.   <name>Isaac Hayes</name>
  10.   <birth_year>1942</birth_year>
  11.   <genre>Soul</genre>
  12. </artist>
  13. <artist>
  14.   <name>Ray Charles</name>
  15.   <birth_year>1930</birth_year>
  16.   <genre>Country and Soul</genre>
  17. </artist>
  18. </favorite_artists>

Here goes an example with the rootTag being used at a lower level:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.     "SELECT
  7.         *
  8.      FROM
  9.         artist
  10.         LEFT JOIN album ON album.artist_id = artist.artistid",
  11.     array(
  12.         'rootTag' => 'music_library',
  13.         'rowTag' => 'artist',
  14.         'idColumn' => 'artistid',
  15.         'elements' => array(
  16.             'artistid',
  17.             'name',
  18.             'birth_year',
  19.             'birth_place',
  20.             'genre',
  21.             'albums' => array(
  22.                 'rootTag' => 'albums',
  23.                 'rowTag' => 'album',
  24.                 'idColumn' => 'albumid',
  25.                 'elements' => array(
  26.                     'albumid',
  27.                     'title',
  28.                     'published_year',
  29.                     'comment'
  30.                 )
  31.             )
  32.         )
  33.     )
  34. );
  35.  
  36. header('Content-Type: application/xml');
  37. $dom->formatOutput true;
  38. print $dom->saveXML();
  39. ?>
['elements']['albums']['rootTag'] is set to 'albums'. Therefore all 'album' tags of a single artist will be enclosed by a singel 'albums' tag. This example is actually taken from Case 02: LEFT OUTER JOIN, so please see there for the resulting XML data and further discussion.

As shown in Case 04: Case 03 with custom tag names, attributes, merge_selective and more is is also possible to assign an empty string to the rootTag option or to omit it at all. In our case this results in all the album tags not being surrounded by a single 'albums' tag but being directly placed inside the 'artist' tag:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.     "SELECT
  7.         *
  8.      FROM
  9.         artist
  10.         LEFT JOIN album ON album.artist_id = artist.artistid",
  11.     array(
  12.         'rootTag' => 'music_library',
  13.         'rowTag' => 'artist',
  14.         'idColumn' => 'artistid',
  15.         'elements' => array(
  16.             'artistid',
  17.             'name',
  18.             'birth_year',
  19.             'birth_place',
  20.             'genre',
  21.             'albums' => array(
  22.                 'rowTag' => 'album',
  23.                 'idColumn' => 'albumid',
  24.                 'elements' => array(
  25.                     'albumid',
  26.                     'title',
  27.                     'published_year',
  28.                     'comment'
  29.                 )
  30.             )
  31.         )
  32.     )
  33. );
  34.  
  35. header('Content-Type: application/xml');
  36. $dom->formatOutput true;
  37. print $dom->saveXML();
  38. ?>
The resulting XML looks like this:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <music_library>
  3.   <artist>
  4.     <artistid>1</artistid>
  5.     <name>Curtis Mayfield</name>
  6.     <birth_year>1920</birth_year>
  7.     <birth_place>Chicago</birth_place>
  8.     <genre>Soul</genre>
  9.     <album>
  10.       <albumid>1</albumid>
  11.       <title>New World Order</title>
  12.       <published_year>1990</published_year>
  13.       <comment>the best ever!</comment>
  14.     </album>
  15.     <album>
  16.       <albumid>2</albumid>
  17.       <title>Curtis</title>
  18.       <published_year>1970</published_year>
  19.       <comment>that man's got somthin' to say</comment>
  20.     </album>
  21.   </artist>
  22.   <artist>
  23.     <artistid>2</artistid>
  24.     <name>Isaac Hayes</name>
  25.     <birth_year>1942</birth_year>
  26.     <birth_place>Tennessee</birth_place>
  27.     <genre>Soul</genre>
  28.     <album>
  29.       <albumid>3</albumid>
  30.       <title>Shaft</title>
  31.       <published_year>1972</published_year>
  32.       <comment>he's the man</comment>
  33.     </album>
  34.   </artist>
  35.   <artist>
  36.     <artistid>3</artistid>
  37.     <name>Ray Charles</name>
  38.     <birth_year>1930</birth_year>
  39.     <birth_place>Mississippi</birth_place>
  40.     <genre>Country and Soul</genre>
  41.   </artist>
  42. </music_library>

Note however that a hidden child element is used as a container to ensure the order of the generated XML elements. Internally all elements with a name that starts with '__' are hidden. An explicit definition of the hidden complex element would look like this:
  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.     "SELECT
  7.         *
  8.      FROM
  9.         artist
  10.         LEFT JOIN album ON album.artist_id = artist.artistid",
  11.     array(
  12.         'rootTag' => 'music_library',
  13.         'rowTag' => 'artist',
  14.         'idColumn' => 'artistid',
  15.         'elements' => array(
  16.             'artistid',
  17.             'name',
  18.             'birth_year',
  19.             'birth_place',
  20.             'genre',
  21.             'albums' => array(
  22.                 'rootTag' => '__albums',
  23.                 'rowTag' => 'album',
  24.                 'idColumn' => 'albumid',
  25.                 'elements' => array(
  26.                     'albumid',
  27.                     'title',
  28.                     'published_year',
  29.                     'comment'
  30.                 )
  31.             )
  32.         )
  33.     )
  34. );
  35.  
  36. header('Content-Type: application/xml');
  37. $dom->formatOutput true;
  38. print $dom->saveXML();
  39. ?>

$options['value']

The value of an XML element's child text node. The specification can be of the following types:

  • COLUMN NAME: this is the default if not preceeded by ':' or '#'. If the column does not exist, an XML_Query2XML_ConfigException will be thrown.
  • STATIC TEXT with a : prefix: if the value is preceeded by a colon (':'), it is interpreted as static text.
  • CALLBACK FUNCTION with a # prefix: if the value is preceeded by a pound sign ('#'), it is interpreted as a callback function. You can use a regular function (e.g. '#myFunction()') or a static method (e.g. '#MyClass::myFunction()') - for how to use a non-static method, see the type COMMAND OBJECT. The current record will be passed to the callback function as an associative array. You can also pass additional string arguments to the callback function by specifing them within the opening and closing brace; e.g. '#Utils::limit(12)' will result in Util::limit() being called with the current record as the first and '12' as the second argument. If you do not want to pass additional arguments to the callback function, the opening and closing brace are optional. The callback function's return value will be converted to a string and used as the child text node if it is anything but an object or an array. If you do return an object or an array from a callback function it has to be an instance of DOMNode or an array of DOMNode instances. Please see Integrating other XML data sources for examples and further details. If an instances of any other class is returned, a XML_Query2XML_XMLException will be thrown.
  • COMMAND OBJECT (object): If you want to use a non-static method as a callback function, you can do so by specifying the value as an instance of a class that implements the XML_Query2XML_Callback interface. This implementation of the command pattern gives you all the flexibility. The disadvantage ist that you cannot use the XML UNSERIALIZATION prefix or the CONDITIONAL prefix. Note: you have to require_once 'XML/Query2XML/Callback.php' before using the XML_Query2XML_Callback interface. The return value of a COMMAND OBJECT's execute() method is treated exactly the same as the return value of a CALLBACK FUNCTION.
There are four more prefixes available that can be used in conjunction with all the prifixes described above:
  • XML UNSERIALIZATION prefix &: the ampersand (&) prefix allows you to automatically unserialize string data, i.e. transform a string into a DOMDocument. DOMDocument's loadXML() method will be used for this purpose. You can combine all three types with this prefix: '&COLUMN_NAME', '&#function()' or '&:<name>John</name>' will all work. You can even use the CONDITIONAL prefix which has to preceed all other prefixes. If the data cannot be unserialized i.e. DOMDocument::loadXML() returns false, a XML_Query2XML_XMLException will be thrown. Please see Integrating other XML data sources for examples and further details.
  • BASE64 ENCODING prefix ^: if the specification starts with a carrat sign ('^'), the element value will be passed to base64_encode(). The BASE64 ENCODING prefix can be used with all the prefixes described above (just put the BASE64 ENCODING prefix first): e.g. '^#', '^:' or '^COLUMN_NAME'.
  • CDATA SECTION prefix =: if the specification starts with an equal sign ('='), the element value will be enclosed in a CDATA section. A CDATA section starts with "<![CDATA[" and ends with "]]>". The CDATA SECTION prefix can be used with all the prefixes described above (just put the CDATA SECTION prefix first): e.g. '=#', '=:', '=COLUMN_NAME' or '=^'.
  • CONDITIONAL prefix ?: if the specification starts with a question mark ('?'), the whole element will be skipped if the value equals (==) an empty string. The CONDITIONAL prefix can be combined with all types described above: if you do this you have to write the CONDITIONAL prefix first e.g. '?#', '?:', '?&', '?=', '?^', or '?COLUMN_NAME'.
Note: for ovious reasons, the XML UNSERIALIZATION prefix and the CONDITIONAL prefix cannot be combined with a COMMAND OBJECT.

Basically, the same syntax can be use for Simple Element Specifications, Simple Attribute Specifications, Complex Query Specification and $options['idColumn'] because the private method XML_Query2XML::_applyColumnStringToRecord() is used in all cases.

The following example demonstrates the usage of some of the types (for a full demonstration of all types see the second example under Simple Element Specifications). The comment element will be skipped if its value == "". Same holds true for the genre element which uses the trim'ed version of the value stored in the genre column. The comment tag has an attribute named type with a static value of "short text". The published_century element gets the century calculated using floor and has the attribute digitCount with a static value of 2.

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.   "SELECT
  7.     *
  8.    FROM
  9.     album al,
  10.     artist ar
  11.    WHERE
  12.     al.artist_id = ar.artistid",
  13.   array(
  14.     'rootTag' => 'albums',
  15.     'idColumn' => 'albumid',
  16.     'rowTag' => 'album',
  17.     'elements' => array(
  18.         'albumid',
  19.         'title',
  20.         'published_year',
  21.         'published_century' => array(
  22.             'value' => "#Utils::getPublishedYearCentury()",
  23.             'attributes' => array(
  24.                 'digitCount' => ':2'
  25.             )
  26.         ),
  27.         'comment' => array(
  28.             'value' => '?comment',
  29.             'attributes' => array(
  30.                 'type' => ':short text'
  31.             )
  32.         ),
  33.         'genre' => array(
  34.             'value' => "?#Utils::trimGenre()"
  35.         )
  36.     )
  37.   )
  38. );
  39. header('Content-Type: application/xml');
  40. $dom->formatOutput true;
  41. print $dom->saveXML();
  42.  
  43. class Utils
  44. {
  45.     function trimGenre($record)
  46.     {
  47.         return trim($record['genre']);
  48.     }
  49.     
  50.     function getPublishedYearCentury($record)
  51.     {
  52.         return floor($record['published_year']/100);
  53.     }
  54. }
  55. ?>
The resulting XML data looks like this:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <albums>
  3.   <album>
  4.     <albumid>1</albumid>
  5.     <title>New World Order</title>
  6.     <published_year>1990</published_year>
  7.     <published_century digitCount="2">19</published_century>
  8.     <comment type="short text">the best ever!</comment>
  9.     <genre>Soul</genre>
  10.   </album>
  11.   <album>
  12.     <albumid>2</albumid>
  13.     <title>Curtis</title>
  14.     <published_year>1970</published_year>
  15.     <published_century digitCount="2">19</published_century>
  16.     <comment type="short text">that man's got somthin' to say</comment>
  17.     <genre>Soul</genre>
  18.   </album>
  19.   <album>
  20.     <albumid>3</albumid>
  21.     <title>Shaft</title>
  22.     <published_year>1972</published_year>
  23.     <published_century digitCount="2">19</published_century>
  24.     <comment type="short text">he's the man</comment>
  25.     <genre>Soul</genre>
  26.   </album>
  27. </albums>

$options['condition']

This option allows you to specify a condition for the element to be included. The string assigned to the condition option can be of the following types:

  • COLUMN NAME: this is the default if not preceeded by ':' or '#'. If the column does not exist, an XML_Query2XML_ConfigException will be thrown. Remember that the string '0' or '' will both evaluate to false which means that the element would be skipped. Note: in most cases you will be much better off changing your WHERE clause than using this type of condition.
  • STATIC TEXT with a : prefix: if the value is preceeded by a colon (':'), it is interpreted as static text. Remember that the string '0' or '' will both evaluate to false which means that the element would be skipped.
  • CALLBACK FUNCTION with a # prefix: if the value is preceeded by a pound sign ('#'), it is interpreted as a callback function. You can use a regular function (e.g. '#myFunction()') or a static method (e.g. '#MyClass::myFunction()') - for how to use a non-static method, see the type COMMAND OBJECT. The current record will be passed to the callback function as an associative array. You can also pass additional string arguments to the callback function by specifing them within the opening and closing brace; e.g. '#Utils::limit(12)' will result in Util::limit() being called with the current record as the first and '12' as the second argument. If you do not want to pass additional arguments to the callback function, the opening and closing brace are optional.
  • COMMAND OBJECT (object): If you want to use a non-static method as a callback function, you can do so by specifying the value as an instance of a class that implements the XML_Query2XML_Callback interface. This implementation of the command pattern gives you all the flexibility. Note: you have to require_once 'XML/Query2XML/Callback.php' before using the XML_Query2XML_Callback interface. The return value of a COMMAND OBJECT's execute() method is treated exactly the same as the return value of a CALLBACK FUNCTION.
This option provides a similar function as the "?" prefix for column specifications - see Simple Element Specifications, Simple Attribute Specifications and $options['value']. The difference is that $options['condition'] is more powerful: you can call any external function you like to determin whether the element shall be included. Here goes a little example:
  1. <?php
  2. if (isset($_REQUEST['includeCondition'])) {
  3.     $includeCondition ($_REQUEST['includeCondition'== '1');
  4. else {
  5.     $includeCondition false;
  6. }
  7. require_once 'XML/Query2XML.php';
  8. require_once 'MDB2.php';
  9. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  10. $dom =$query2xml->getXML(
  11.     "SELECT
  12.         *
  13.      FROM
  14.         artist
  15.         LEFT JOIN album ON album.artist_id = artist.artistid",
  16.     array(
  17.         'rootTag' => 'music_library',
  18.         'rowTag' => 'artist',
  19.         'idColumn' => 'artistid',
  20.         'elements' => array(
  21.             'artistid',
  22.             'name',
  23.             'birth_year',
  24.             'birth_place',
  25.             'genre',
  26.             'albums' => array(
  27.                 'rootTag' => 'albums',
  28.                 'rowTag' => 'album',
  29.                 'idColumn' => 'albumid',
  30.                 'condition' => '#isSpecialPublishedYear()',
  31.                 'elements' => array(
  32.                     'albumid',
  33.                     'title',
  34.                     'published_year',
  35.                     'comment' => array(
  36.                         'value' => 'comment',
  37.                         'condition' => ':' ($includeCondition '1' '0')
  38.                     )
  39.                 )
  40.             )
  41.         )
  42.     )
  43. );
  44. header('Content-Type: application/xml');
  45. $dom->formatOutput true;
  46. print $dom->saveXML();
  47.  
  48. /**Returns whether $year is 1970 or 1972.
  49. */
  50. function isSpecialPublishedYear($record)
  51. {
  52.     //do some highly complex calculations ...
  53.     return $record['published_year'== 1970 || $record['published_year'== 1972;
  54. }
  55. ?>
The resulting XML data is:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <music_library>
  3.   <artist>
  4.     <artistid>1</artistid>
  5.     <name>Curtis Mayfield</name>
  6.     <birth_year>1920</birth_year>
  7.     <birth_place>Chicago</birth_place>
  8.     <genre>Soul</genre>
  9.     <albums>
  10.       <album>
  11.         <albumid>2</albumid>
  12.         <title>Curtis</title>
  13.         <published_year>1970</published_year>
  14.       </album>
  15.     </albums>
  16.   </artist>
  17.   <artist>
  18.     <artistid>2</artistid>
  19.     <name>Isaac Hayes</name>
  20.     <birth_year>1942</birth_year>
  21.     <birth_place>Tennessee</birth_place>
  22.     <genre>Soul</genre>
  23.     <albums>
  24.       <album>
  25.         <albumid>3</albumid>
  26.         <title>Shaft</title>
  27.         <published_year>1972</published_year>
  28.       </album>
  29.     </albums>
  30.   </artist>
  31.   <artist>
  32.     <artistid>3</artistid>
  33.     <name>Ray Charles</name>
  34.     <birth_year>1930</birth_year>
  35.     <birth_place>Mississippi</birth_place>
  36.     <genre>Country and Soul</genre>
  37.     <albums />
  38.   </artist>
  39. </music_library>

Note that (if present) $options['sql'] will get processed *before* evaluating the condition. This allows you to wirte code like the following:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.   "SELECT
  7.     *
  8.    FROM
  9.     artist",
  10.   array(
  11.     'rootTag' => 'artists',
  12.     'idColumn' => 'artistid',
  13.     'rowTag' => 'artist',
  14.     'elements' => array(
  15.         'artistid',
  16.         'name',
  17.         'albums' => array(
  18.             'idColumn' => 'albumid',
  19.             'sql' => array(
  20.                 'data' => array(
  21.                     'artistid'
  22.                 ),
  23.                 'query' => "SELECT * FROM album WHERE artist_id = ?",
  24.             ),
  25.             'condition' => '#isGT1980()',
  26.             'elements' => array(
  27.                 'title',
  28.                 'published_year'
  29.             )
  30.         )
  31.     )
  32.   )
  33. );
  34. header('Content-Type: application/xml');
  35. $dom->formatOutput true;
  36. print $dom->saveXML();
  37.  
  38. function isGT1980($record)
  39. {
  40.     return $record['published_year'1980;
  41. }
  42. ?>
"published_year" is a column of the table album but as the "sql" option is processed before evaluating the "condition" option everything works just fine:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <artists>
  3.   <artist>
  4.     <artistid>1</artistid>
  5.     <name>Curtis Mayfield</name>
  6.     <albums>
  7.       <title>New World Order</title>
  8.       <published_year>1990</published_year>
  9.     </albums>
  10.   </artist>
  11.   <artist>
  12.     <artistid>2</artistid>
  13.     <name>Isaac Hayes</name>
  14.   </artist>
  15.   <artist>
  16.     <artistid>3</artistid>
  17.     <name>Ray Charles</name>
  18.   </artist>
  19. </artists>

$options['sql']

Note: This option is driver-specific. The following discussion is limited to the database-related drivers.

This and $options['sql_options'] are the only options that can only be present within Complex Element Specifications. If given at the root level, it would be just ignored. $options['sql'] allows you to split up one huge JOIN into multiple smaller queries. You might want (or have) to do this in several scenarios:

  • Your RDBMS has a maximum number of fields it can return in a single query and you've reached it.
  • You are short on memory: let's say your big JOIN returns 100 fields and you have 10 000 records. It might turn out that the memory consumption is lower if you split up the single big JOIN into multiple quieres that have smaller result sets. As all the data won't be in memory at once, it might even run faster.
  • You are too lazy to think about how to best join these 8 tables :)
You will definitively want to do some Profiling and Performance Tuning before deciding whether or not to split up one big JOIN into multiple smaller JOINs.

There are two ways of specifying $options['sql']:

  • Simple Query Specification: uses the query() method provided by the database abstraction layer (PDO/MDB2/DB/ADOdb) - use it with care
  • Complex Query Specification: uses the prepare() and execute() methods provided by the database abstraction layer and can therefore prevent SQL injection and is also faster in most scenarios

Simple Query Specification

Since v0.8.0 a simple query specifications are purely static strings (in most cases you will want to use a Complex Query Specification):

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.     "SELECT
  7.         *
  8.      FROM
  9.         artist
  10.      WHERE
  11.         artistid = 1",
  12.     array(
  13.         'rootTag' => 'music_library',
  14.         'rowTag' => 'artist',
  15.         'idColumn' => 'artistid',
  16.         'elements' => array(
  17.             'artistid',
  18.             'name',
  19.             'birth_year',
  20.             'birth_place',
  21.             'genre',
  22.             'albums' => array(
  23.                 'sql' => 'SELECT * FROM album WHERE artist_id = 1',
  24.                 'rootTag' => 'albums',
  25.                 'rowTag' => 'album',
  26.                 'idColumn' => 'albumid',
  27.                 'elements' => array(
  28.                     'albumid',
  29.                     'title',
  30.                     'published_year',
  31.                     'comment'
  32.                 )
  33.             )
  34.         )
  35.     )
  36. );
  37.  
  38. header('Content-Type: application/xml');
  39. $dom->formatOutput true;
  40. print $dom->saveXML();
  41. ?>

To understand how $options['sql'] really works, some knowledge of XML_Query2XML's internals might be helpful: XML_Query2XML::getXML() calls the private method XML_Query2XML::_getNestedXMLRecord() for every record retrieved from the database using the SQL statement passed to getXML() as first argument. XML_Query2XML::_getNestedXMLRecord() will then process the current record according to the settings specified in $options. The processing of all Complex Element Specifications is handed off to the private method XML_Query2XML::_processComplexElementSpecification(). XML_Query2XML::_processComplexElementSpecification() in turn will call the private method XML_Query2XML::_applySqlOptionsToRecord() to interpret $options['sql'] and $options['sql_options']. XML_Query2XML::_processComplexElementSpecification() will then call again XML_Query2XML::_getNestedXMLRecord() for every record retrieved using the query specified in the 'sql' option.


Complex Query Specification

A Complex Query Specification uses the database abstraction layer's prepare() and execute() methods and therefore prevents SQL injection and is also faster than a Simple Query Specification in most scenarios. It can consist of multiple parts (only $options['sql']['query'] is mandatory):

  • $options['sql']['query']: the SQL query as a string that contains a placeholder for each element of $options['sql']['data'].
  • $options['sql']['driver']: allows you to use a different XML_Query2XML_Driver for this complex query than the one passed to XML_Query2XML::factory(). Please see Using Multiple Drivers for details. $options['sql']['driver'] is optional.
  • $options['sql']['limit']: allows you to limit the number of records returned from the query. It has to be a numeric value. Please note that a value of 0 (or '0') is equivalent to not setting $options['sql']['limit'] at all. $options['sql']['limit'] and $options['sql']['offset'] are only interpreted by the drivers for PEAR MDB2 and PEAR DB. All other drivers simply ignore these two options.
  • $options['sql']['offset']: allows you to set the number of the first record to retrieve. This has to be a numeric value. The default is 0. Please note that this option will be ignored unless $options['sql']['limit'] is set. $options['sql']['offset'] and $options['sql']['limit'] are only interpreted by the drivers for PEAR MDB2 and PEAR DB. All other drivers simply ignore these two options.
  • $options['sql']['data']: an indexed array of values. This is optional. The specification can be of the following types:
    • COLUMN NAME: this is the default if not preceeded by ':' or '#'. If the column does not exist, an XML_Query2XML_ConfigException will be thrown. Note that the parent record will be used! This is quite logic as this SQL statement has not been executed yet :)
    • STATIC TEXT with a : prefix: if the value is preceeded by a colon (':'), it is interpreted as static text.
    • CALLBACK FUNCTION with a # prefix: if the value is preceeded by a pound sign ('#'), it is interpreted as a callback function. You can use a regular function (e.g. '#myFunction()') or a static method (e.g. '#MyClass::myFunction()') - for how to use a non-static method, see the type COMMAND OBJECT. The current record will be passed to the callback function as an associative array. You can also pass additional string arguments to the callback function by specifing them within the opening and closing brace; e.g. '#Utils::limit(12)' will result in Util::limit() being called with the current record as the first and '12' as the second argument. If you do not want to pass additional arguments to the callback function, the opening and closing brace are optional.
    • COMMAND OBJECT (object): If you want to use a non-static method as a callback function, you can do so by specifying the value as an instance of a class that implements the XML_Query2XML_Callback interface. This implementation of the command pattern gives you all the flexibility. Note: you have to require_once 'XML/Query2XML/Callback.php' before using the XML_Query2XML_Callback interface. The return value of a COMMAND OBJECT's execute() method is treated exactly the same as the return value of a CALLBACK FUNCTION.
    The same syntax (with the additional '?' prefix) can be use for Simple Element Specifications, $options['value'], Simple Attribute Specifications and $options['idColumn'] because the private method XML_Query2XML::_applyColumnStringToRecord() is used in all cases. Note: $options['sql']['data'] is optional!
Here is a simple example similar to Case 03: Two SELECTs instead of a LEFT OUTER JOIN:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.     "SELECT
  7.         *
  8.      FROM
  9.         artist",
  10.     array(
  11.         'rootTag' => 'music_library',
  12.         'rowTag' => 'artist',
  13.         'idColumn' => 'artistid',
  14.         'elements' => array(
  15.             'artistid',
  16.             'name',
  17.             'birth_year',
  18.             'birth_place',
  19.             'genre',
  20.             'albums' => array(
  21.                 'sql' => array(
  22.                     'data' => array(
  23.                         'artistid'
  24.                     ),
  25.                     'query' => "SELECT * FROM album WHERE artist_id = ?"
  26.                 ),
  27.                 'rootTag' => 'albums',
  28.                 'rowTag' => 'album',
  29.                 'idColumn' => 'albumid',
  30.                 'elements' => array(
  31.                     'albumid',
  32.                     'title',
  33.                     'published_year',
  34.                     'comment'
  35.                 )
  36.             )
  37.         )
  38.     )
  39. );
  40.  
  41. header('Content-Type: application/xml');
  42. $dom->formatOutput true;
  43. print $dom->saveXML();
  44. ?>


$options['sql_options']

This allows you to specify how $options['sql'] is handled. $options['sql_options'] is an associative array that can have the following fileds:

Per default all options are set to the boolean value false.

$options['sql_options']['cached']

Since 1.5.0RC1 Caching is deactivated by default. If caching is activated the result of a query is stored in the private associative array XML_Query2XML::$_recordCache using the SQL query string as key. If the exact same query needs to be executed a second time, its results can be retrieved from cache.

Before setting $options['sql_options']['cached'] to true, do some Profiling and Performance Tuning. As documented in XML_Query2XML::getProfile() the CACHED column in the profile output will show 'true!' if caching is performed without being necessary.

Caching only makes sense, if you have to run exactly the same query multiple times.


$options['sql_options']['single_record']

Use this option to make sure that the SQL query you specified in $options['sql'] returns only a single record. This option is in fact of limited use. Do not use it to fetch only the first record from a large result set. (SQL is your friend: use a better WHERE clause!)


$options['sql_options']['merge']

By default no merging is done so that less memory is used. This means that the data of the record present on the parent level will not be available at this level. Only the data returned by $options['sql'] will be available (and therefore use up memory). If you also need the data of the record present on the parent level the two arrays have to be merged using array_merge(). If $options['sql'] returned multiple records, each of them has to be merged with the one of the parent level separatly:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.     "SELECT
  7.         *
  8.      FROM
  9.         artist",
  10.     array(
  11.         'rootTag' => 'MUSIC_LIBRARY',
  12.         'rowTag' => 'ARTIST',
  13.         'idColumn' => 'artistid',
  14.         'elements' => array(
  15.             'NAME' => 'name',
  16.             'BIRTH_YEAR' => 'birth_year',
  17.             'GENRE' => 'genre',
  18.             'albums' => array(
  19.                 'sql' => array(
  20.                     'data' => array('artistid'),
  21.                     'query' => "SELECT * FROM album WHERE artist_id = ?"
  22.                 ),
  23.                 'sql_options' => array(
  24.                     'merge' => true
  25.                 ),
  26.                 'rootTag' => '',
  27.                 'rowTag' => 'ALBUM',
  28.                 'idColumn' => 'albumid',
  29.                 'elements' => array(
  30.                     'TITLE' => 'title',
  31.                     'PUBLISHED_YEAR' => 'published_year',
  32.                     'COMMENT' => 'comment',
  33.                     'GENRE' => 'genre'
  34.                 )
  35.             )
  36.         )
  37.     )
  38. );
  39.  
  40. header('Content-Type: application/xml');
  41. $dom->formatOutput true;
  42. print $dom->saveXML();
  43. ?>
This produces quite some overhead. It is therefore highly recommended to use $options['sql_options']['merge_selective'] described in the next section.


$options['sql_options']['merge_selective']

As a full merge with the parent record might severly affect the performance, the sql option merge_selective allows you to only merge the current record with specific columns of the parent record. Just place the names of all columns of the parent record you want to be available in the current record in an array and assign it to the merge_selective option. Here goes an example:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.     "SELECT
  7.         *
  8.      FROM
  9.         artist",
  10.     array(
  11.         'rootTag' => 'MUSIC_LIBRARY',
  12.         'rowTag' => 'ARTIST',
  13.         'idColumn' => 'artistid',
  14.         'elements' => array(
  15.             'NAME' => 'name',
  16.             'BIRTH_YEAR' => 'birth_year',
  17.             'GENRE' => 'genre',
  18.             'albums' => array(
  19.                 'sql' => array(
  20.                     'data' => array('artistid'),
  21.                     'query' => "SELECT * FROM album WHERE artist_id = ?"
  22.                 ),
  23.                 'sql_options' => array(
  24.                     'merge_selective' => array('genre')
  25.                 ),
  26.                 'rootTag' => '',
  27.                 'rowTag' => 'ALBUM',
  28.                 'idColumn' => 'albumid',
  29.                 'elements' => array(
  30.                     'TITLE' => 'title',
  31.                     'PUBLISHED_YEAR' => 'published_year',
  32.                     'COMMENT' => 'comment',
  33.                     'GENRE' => 'genre'
  34.                 )
  35.             )
  36.         )
  37.     )
  38. );
  39.  
  40. header('Content-Type: application/xml');
  41. $dom->formatOutput true;
  42. print $dom->saveXML();
  43. ?>
Please see Case 04: Case 03 with custom tag names, attributes, merge_selective and more for a similar example and more discussion of $options['sql_options']['merge_selective'].


$options['sql_options']['merge_master']

If (selective) merging is performed, it might become important which record overwrites the data of the other. As soon as both result sets have a column with the same name, there is a confilict that has to be resolved. By default, the record of the parent level is the master and overwrites the record(s) returned by $options['sql']. If you want the new records to overwrite the record of the parent level, set $options['sql_options']['merge_master'] to true. Note that this option only has an effect if $options['sql_options']['merge'] is set to true or $options['sql_options']['merge_selective'] is used.


$options['mapper']

This option allows you to specifiy a function for mapping SQL identifiers to XML names. Whenever you use a Simple Element Specification or a Simple Attribute Specification only with a column name and without a tag/attribute name, the specified column name will be used for the tag/attribute name. Please note that mapping is also performed when the Using the Asterisk Shortcut is used. Per default $options['mapper'] is set to false which means that no special mapping is used. $options['mapper'] can have one of the following formats:

  • 'CLASS::STATIC_METHOD': this syntax allows you to use a static method for mapping:
    1. 'mapper' => 'MyMapper::map'
  • array('CLASS', 'STATIC_METHOD'): this syntax also allows you to use a static method for mapping:
    1. 'mapper' => array('MyMapper''map')
  • array($instance, 'METHOD'): this syntax allows you to use a non-static method for mapping:
    1. 'mapper' => array($myMap'map')
  • 'FUNCTION': this syntax allows you to use a regular function for mapping:
    1. 'mapper' => 'myUppercaseMapper'
  • false: use the boolean value false (or any other value that == false) to deactivate any special mapping:
    1. 'mapper' => false
Remember that the mapping only applies to Simple Element Specifications and Simple Attribute Specifications that do not explicitly have a tag/attribute name or those that have a tag/attribute name that contains an asterisk shortcut. The following example will also show that a mapper defined at the root level is also used at all lower levels (unless it gets overwritten, see Using multiple mappers):
  1. <?php
  2. class SomeMapper
  3. {
  4.     public function map($str)
  5.     {
  6.         //do something with $str
  7.         return $str;
  8.     }
  9. }
  10.  
  11. require_once 'XML/Query2XML.php';
  12. require_once 'XML/Query2XML/ISO9075Mapper.php';
  13. require_once 'MDB2.php';
  14. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  15. $dom $query2xml->getXML(           //
  16.   "SELECT * FROM artist",            //
  17.   array(                             //
  18.     'rootTag' => 'favorite_artists'//no mapping
  19.     'idColumn' => 'artistid',        //nothing to map
  20.     'rowTag' => 'artist',            //no mapping
  21.     'mapper' => 'SomeMapper::map',   //
  22.     'elements' => array(             //
  23.       'artistid',                 //mapping
  24.       'NAME' => 'name',           //no mapping as the tag name is specified
  25.       '*',                        //mapping
  26.       'TAG_*' => '*',             //does a mapping too!
  27.       'albums' => array(          //nothing to map
  28.         'sql' => array(           //
  29.           'data' => array(        //
  30.             'artistid'            //nothing to map
  31.           ),                      //
  32.           'query' => 'SELECT * FROM album WHERE artist_id = ?'      //
  33.         ),                      //
  34.         'rootTag' => 'albums',  //no mapping
  35.         'rowTag' => 'album',    //no mapping
  36.         'idColumn' => 'albumid',//nothing to map
  37.         'elements' => array(    //
  38.           'albumid',          //mapping using the mapper specified at the root level
  39.           'title',            //mapping using the mapper specified at the root level
  40.           'published_year',   //mapping using the mapper specified at the root level
  41.           'comment'           //mapping using the mapper specified at the root level
  42.         )                     //
  43.       )                       //
  44.     ),                        //
  45.     'attributes' => array(        //
  46.       'artistid',                 //mapping
  47.       'NAME' => 'name',           //no mapping as the tag name is specified
  48.       '*',                        //mapping
  49.       'TAG_*' => '*'              //does a mapping too!
  50.     )                             //
  51.   )                               //
  52. );                                //
  53. header('Content-Type: application/xml');    //
  54. print $dom->saveXML();                      //
  55. ?>

Mapping SQL identifiers to XML names in accordance with ISO/IEC 9075-14:2005

The package XML_Query2XML also implements the Final Committee Draft for ISO/IEC 9075-14:2005, section "9.1 Mapping SQL <identifier>s to XML Names". ISO/IEC 9075-14:2005 is available online at http://www.sqlx.org/SQL-XML-documents/5FCD-14-XML-2004-07.pdf.

A lot of characters are legal in SQL identifiers but cannot be used within XML names. To begin with, SQL identifiers can contain any Unicode character while XML names are limited to a certain set of characters. E.g the SQL identifier "<21yrs in age" obviously is not a valid XML name. '#', '{', and '}' are also not allowed. Fully escaped SQL identifiers also must not contain a column (':') or start with "xml" (in any case combination). Illegal characters are mapped to a string of the form _xUUUU_ where UUUU is the Unicode value of the character.

The following is a table of example mappings:

+----------------+------------------------+------------------------------------+
| SQL-Identifier | Fully escaped XML name | Comment                            |
+----------------+------------------------+------------------------------------+
| dept:id        | dept_x003A_id          | ":" is illegal                     |
| xml_name       | _x0078_ml_name         | must not start with [Xx][Mm][Ll]   |
| XML_name       | _x0058_ML_name         | must not start with [Xx][Mm][Ll]   |
| hire date      | hire_x0020_date        | space is illegal too               |
| Works@home     | Works_x0040_home       | "@" is illegal                     |
| file_xls       | file_x005F_xls         | "_" gets mapped if followed by "x" |
| FIRST_NAME     | FIRST_NAME             | no problem here                    |
+----------------+------------------------+------------------------------------+
     

The ISO 9075-mapping does produce some overhead which might not be needed in a lot of situations. Therefore it is not the default mapper. In most cases it will be sufficient to validate your XML schema once using tools like the free XMLSpy Home Edition.

To use the ISO 9075-mapper that comes with XML_Query2XML you have to:

Here goes an example:
  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'XML/Query2XML/ISO9075Mapper.php';
  4. require_once 'MDB2.php';
  5. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  6. $dom $query2xml->getXML(
  7.   "SELECT * FROM artist",
  8.   array(
  9.     'rootTag' => 'favorite_artists',
  10.     'idColumn' => 'artistid',
  11.     'rowTag' => 'artist',
  12.     'mapper' => 'XML_Query2XML_ISO9075Mapper::map',
  13.     'elements' => array(
  14.         '*'
  15.     )
  16.   )
  17. );
  18. header('Content-Type: application/xml');
  19. $dom->formatOutput true;
  20. print $dom->saveXML();
  21. ?>


Building your own mappers

There are cases when you will want the tag and attribute names to be somehow different from the column names. Let's say you want to use the column names as tag and attribute names but make them all uppercase. Certainly you could write code like this:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'XML/Query2XML/ISO9075Mapper.php';
  4. require_once 'MDB2.php';
  5. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  6. $dom $query2xml->getXML(
  7.   "SELECT * FROM artist",
  8.   array(
  9.     'rootTag' => 'favorite_artists',
  10.     'idColumn' => 'artistid',
  11.     'rowTag' => 'artist',
  12.     'elements' => array(
  13.         'NAME' => 'name',
  14.         'BIRTH_YEAR' => 'birth_year',
  15.         'BIRTH_PLACE' => 'birth_place',
  16.         'GENRE' => 'genre',
  17.     ),
  18.     'attributes' => array(
  19.         'ARTISTID' => 'artistid'
  20.     )
  21.   )
  22. );
  23. header('Content-Type: application/xml');
  24. $dom->formatOutput true;
  25. print $dom->saveXML();
  26. ?>
But that seems a little redundant, doesn't it? In cases like these it is recommended to write your own mapper. As we want to write OO code we don't implement our mapper as a function but as a static public method of the new class UppercaseMapper. The mapper must take a string as an argument and must return a string:
  1. <?php
  2. class UppercaseMapper
  3. {
  4.     public function map($str)
  5.     {
  6.         return strtoupper($str);
  7.     }
  8. }
  9.  
  10. require_once 'XML/Query2XML.php';
  11. require_once 'MDB2.php';
  12. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  13. $dom $query2xml->getXML(
  14.   "SELECT * FROM artist",
  15.   array(
  16.     'rootTag' => 'favorite_artists',
  17.     'idColumn' => 'artistid',
  18.     'rowTag' => 'artist',
  19.     'mapper' => 'UppercaseMapper::map',
  20.     'elements' => array(
  21.         'name',
  22.         'birth_year',
  23.         'birth_place',
  24.         'genre',
  25.     ),
  26.     'attributes' => array(
  27.         'artistid'
  28.     )
  29.   )
  30. );
  31. header('Content-Type: application/xml');
  32. $dom->formatOutput true;
  33. print $dom->saveXML();
  34. ?>
The resulting XML data looks like this:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <favorite_artists>
  3.   <artist ARTISTID="1">
  4.     <NAME>Curtis Mayfield</NAME>
  5.     <BIRTH_YEAR>1920</BIRTH_YEAR>
  6.     <BIRTH_PLACE>Chicago</BIRTH_PLACE>
  7.     <GENRE>Soul</GENRE>
  8.   </artist>
  9.   <artist ARTISTID="2">
  10.     <NAME>Isaac Hayes</NAME>
  11.     <BIRTH_YEAR>1942</BIRTH_YEAR>
  12.     <BIRTH_PLACE>Tennessee</BIRTH_PLACE>
  13.     <GENRE>Soul</GENRE>
  14.   </artist>
  15.   <artist ARTISTID="3">
  16.     <NAME>Ray Charles</NAME>
  17.     <BIRTH_YEAR>1930</BIRTH_YEAR>
  18.     <BIRTH_PLACE>Mississippi</BIRTH_PLACE>
  19.     <GENRE>Country and Soul</GENRE>
  20.   </artist>
  21. </favorite_artists>


Using multiple mappers

Let's say we want to force all tags corresponding to columns of the artist table to be uppercase and all tags corresponding to columns of the album table to be lowercase. This can be done using two mappers:

  1. <?php
  2. class MyMappers
  3. {
  4.     public function uppercaseMapper($str)
  5.     {
  6.         return strtoupper($str);
  7.     }
  8.     
  9.     public function lowercaseMapper($str)
  10.     {
  11.         return strtolower($str);
  12.     }
  13. }
  14.  
  15. require_once 'XML/Query2XML.php';
  16. require_once 'MDB2.php';
  17. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  18. $dom $query2xml->getXML(
  19.     "SELECT
  20.         *
  21.      FROM
  22.         artist",
  23.     array(
  24.         'rootTag' => 'music_library',
  25.         'rowTag' => 'artist',
  26.         'idColumn' => 'artistid',
  27.         'mapper' => 'MyMappers::uppercaseMapper',
  28.         'elements' => array(
  29.             '*',
  30.             'albums' => array(
  31.                 'sql' => array(
  32.                     'data' => array(
  33.                         'artistid'
  34.                     ),
  35.                     'query' => 'SELECT * FROM album WHERE artist_id = ?'
  36.                 ),
  37.                 'rootTag' => 'albums',
  38.                 'rowTag' => 'album',
  39.                 'idColumn' => 'albumid',
  40.                 'mapper' => 'MyMappers::lowercaseMapper',
  41.                 'elements' => array(
  42.                     '*',
  43.                     'artist_id' => '?:'
  44.                 )
  45.             )
  46.         )
  47.     )
  48. );
  49. header('Content-Type: application/xml');
  50. $dom->formatOutput true;
  51. print $dom->saveXML();
  52. ?>
As we know that the columns of the album table already are lowercase we could as well use one mapper and just deactivate that for the complex element "albums':
  1. <?php
  2. class MyMappers
  3. {
  4.     public function uppercaseMapper($str)
  5.     {
  6.         return strtoupper($str);
  7.     }
  8. }
  9.  
  10. require_once 'XML/Query2XML.php';
  11. require_once 'MDB2.php';
  12. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  13. $dom $query2xml->getXML(
  14.     "SELECT
  15.         *
  16.      FROM
  17.         artist",
  18.     array(
  19.         'rootTag' => 'music_library',
  20.         'rowTag' => 'artist',
  21.         'idColumn' => 'artistid',
  22.         'mapper' => 'MyMappers::uppercaseMapper',
  23.         'elements' => array(
  24.             '*',
  25.             'albums' => array(
  26.                 'sql' => array(
  27.                     'data' => array(
  28.                         'artistid'
  29.                     ),
  30.                     'query' => 'SELECT * FROM album WHERE artist_id = ?'
  31.                 ),
  32.                 'rootTag' => 'albums',
  33.                 'rowTag' => 'album',
  34.                 'idColumn' => 'albumid',
  35.                 'mapper' => false,
  36.                 'elements' => array(
  37.                     '*',
  38.                     'artist_id' => '?:'
  39.                 )
  40.             )
  41.         )
  42.     )
  43. );
  44. header('Content-Type: application/xml');
  45. $dom->formatOutput true;
  46. print $dom->saveXML();
  47. ?>
In both cases the resulting XML data will look like this:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <music_library>
  3.   <artist>
  4.     <ARTISTID>1</ARTISTID>
  5.     <NAME>Curtis Mayfield</NAME>
  6.     <BIRTH_YEAR>1920</BIRTH_YEAR>
  7.     <BIRTH_PLACE>Chicago</BIRTH_PLACE>
  8.     <GENRE>Soul</GENRE>
  9.     <albums>
  10.       <album>
  11.         <albumid>1</albumid>
  12.         <title>New World Order</title>
  13.         <published_year>1990</published_year>
  14.         <comment>the best ever!</comment>
  15.       </album>
  16.       <album>
  17.         <albumid>2</albumid>
  18.         <title>Curtis</title>
  19.         <published_year>1970</published_year>
  20.         <comment>that man's got somthin' to say</comment>
  21.       </album>
  22.     </albums>
  23.   </artist>
  24.   <artist>
  25.     <ARTISTID>2</ARTISTID>
  26.     <NAME>Isaac Hayes</NAME>
  27.     <BIRTH_YEAR>1942</BIRTH_YEAR>
  28.     <BIRTH_PLACE>Tennessee</BIRTH_PLACE>
  29.     <GENRE>Soul</GENRE>
  30.     <albums>
  31.       <album>
  32.         <albumid>3</albumid>
  33.         <title>Shaft</title>
  34.         <published_year>1972</published_year>
  35.         <comment>he's the man</comment>
  36.       </album>
  37.     </albums>
  38.   </artist>
  39.   <artist>
  40.     <ARTISTID>3</ARTISTID>
  41.     <NAME>Ray Charles</NAME>
  42.     <BIRTH_YEAR>1930</BIRTH_YEAR>
  43.     <BIRTH_PLACE>Mississippi</BIRTH_PLACE>
  44.     <GENRE>Country and Soul</GENRE>
  45.     <albums />
  46.   </artist>
  47. </music_library>


$options['encoder']

This option allows you to specifiy a function/method that performs the XML encoding for node and attribute values. Per default it is assumed that all data is in ISO-8859-1 (Latin-1) and will be encoded to UTF-8 using mb_convert_encoding() or if not available using utf8_encode().

For some introduction to XML encoding please see http://www.w3schools.com/xml/xml_encoding.asp and http://www.opentag.com/xfaq_enc.htm. Note: I highly recommend to use UTF-8 for XML if you don't have a compelling reason to use an other encoding standard.

The default encoding mechanism (ISO-8859-1 to UTF-8) will be just fine in most cases but sometimes your data might already be in in UTF-8 or you might not want your XML to be UTF-8 encoded at all.

Please see XML encoding for how to change the encoding standard used in the XML declaration.

$options['encoder'] can have one of the following formats:

  • 'CLASS::STATIC_METHOD': this syntax allows you to use a static method for encoding:
    1. 'encoder' => 'MyEncoder::encode'
  • array('CLASS', 'STATIC_METHOD'): this syntax also allows you to use a static method for encoding:
    1. 'encoder' => array('MyEncoder''encode')
  • array($instance, 'METHOD'): this syntax allows you to use a non-static method for encoding:
    1. 'encoder' => array($myEncoder'encode')
  • 'FUNCTION': this syntax allows you to use a regular function for encoding:
    1. 'encoder' => 'myUTF8toISO88591Encoder'
  • false: use the boolean value false to deactivate encoding:
    1. 'encoder' => false
  • null: use NULL to reset encoding to the built-in default encoding. This default assumes that all data is in ISO-8859-1 (Latin-1) and will encode it to UTF-8 using mb_convert_encoding() or if not available using utf8_encode().
    1. 'encoder' => null
One thing you should keep in mind when writing your own encoding (wrapper) functions is that the encoder will only be called if the current record has a string value for that column; i.e. the encoder will not be called if the column value is NULL.

The following example will show that an encoder defined at the root level is also used at all lower levels (unless it gets overwritten, see Using multiple encoders):

  1. <?php
  2. class SomeEncoder
  3. {
  4.     public function encode($str)
  5.     {
  6.         //do something with $str
  7.         return $str;
  8.     }
  9. }
  10.  
  11. require_once 'XML/Query2XML.php';
  12. require_once 'XML/Query2XML/ISO9075Mapper.php';
  13. require_once 'MDB2.php';
  14. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  15. $dom $query2xml->getXML(
  16.   "SELECT * FROM artist",
  17.   array(
  18.     'rootTag' => 'favorite_artists',
  19.     'idColumn' => 'artistid',
  20.     'rowTag' => 'artist',
  21.     'encoder' => 'SomeEncoder::encode',     /* we define an encoder at the root level */
  22.     'elements' => array(
  23.       'artistid',                           // encoding will be
  24.       'name',                               // performed on these
  25.       'albums' => array(
  26.         'sql' => array(
  27.           'data' => array(
  28.             'artistid'
  29.           ),
  30.           'query' => 'SELECT * FROM album WHERE artist_id = ?'
  31.         ),
  32.         'rootTag' => 'albums',
  33.         'rowTag' => 'album',
  34.         'idColumn' => 'albumid',
  35.         'elements' => array(
  36.           'albumid',            // encoder setting is affective on all lower
  37.           'title'               // levels
  38.         ),
  39.         'attributes' => array(
  40.           'comment'             // note: encoding is also performed for attributes
  41.         )
  42.       )
  43.     )
  44.   )
  45. );
  46. header('Content-Type: application/xml');
  47. $dom->formatOutput true;
  48. print $dom->saveXML();
  49. ?>

ISO-8859-1 to UTF-8 encoding (default)

This is what will automatically be performed if you do not use $options['encoder'] at all. This is because most databases use ISO-8859-1 (aka Latin-1) by default. As previously stated, XML_Query2XML will use mb_convert_encoding() or if that is not available utf8_encode() for the actual encoding.

If you have set $options['encoder'] on the root level but wish to switch back to the default on a lower level all you have to do is to use the NULL value:

  1. 'encoder' => null


UTF-8 to ISO-8859-1 encoding

If your data is in UTF-8 but you would like your XML to be in ISO-8859-1 (Latin-1), you can use utf8_decode():

  1. 'encoder' => 'utf8_decode'
or define a wrapper for mb_convert_encoding() and use that:
  1. function utf8ToLatin1($str)
  2. {
  3.     //hint: mb_convert_encoding (str, to_encoding, from_encoding)
  4.     return mb_convert_encoding($str'iso-8859-1''UTF-8');
  5. }
specified as encoder:
  1. 'encoder' => 'utf8ToLatin1'


Disabling encoding

If you data already is in the character set you wish to use for the XML, all you have to do is to disable the encoding by using a boolean value of false:

  1. 'encoder' => false


Using multiple encoders

It might happen to you that some of your data sources are in one character set while others are in another. This means that you need different encoding procedures to convert them all to the same character set you wish to use for the XML (usually UTF-8).

In the first example we will assume that all columns of the table artist are in ISO-8859-1 (Latin-1) while all columns of the table album are in UTF-8.

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.     "SELECT
  7.         *
  8.      FROM
  9.         artist
  10.         LEFT JOIN album ON album.artist_id = artist.artistid",
  11.     array(
  12.         'rootTag' => 'music_library',
  13.         'rowTag' => 'artist',
  14.         'idColumn' => 'artistid',
  15.         'elements' => array(    // all columns of the table artist are in
  16.             'artistid',         // ISO-8859-1; the default conversion therefore
  17.             'name',             // is just fine
  18.             'birth_year',
  19.             'birth_place',
  20.             'genre',
  21.             'albums' => array(
  22.                 'rootTag' => 'albums',
  23.                 'rowTag' => 'album',
  24.                 'idColumn' => 'albumid',
  25.                 'encoder' => false,     // the columns of the album table already are in UTF-8;
  26.                 'elements' => array(    // we therefore have to disable encoding
  27.                     'albumid',
  28.                     'title',
  29.                     'published_year',
  30.                     'comment'
  31.                 )
  32.             )
  33.         )
  34.     )
  35. );
  36.  
  37. header('Content-Type: application/xml');
  38.  
  39. $dom->formatOutput true;
  40. print $dom->saveXML();
  41. ?>

For our second example, let's assume that the following columns use the following character sets:

+----------------------------------------+
| Column         | Character Set         |
+----------------------------------------+
| artist.name    | ISO-8859-1 (Latin-1)  |
| artist.genre   | UTF-8                 |
| album.title    | UTF-16                |
| album.comment  | Windows-1252          |
+----------------+-----------------------+
      
As our XML output shall be in UTF-8 we have to use multiple encoders on a per-column basis:
  1. <?php
  2. function latin1ToUTF8($str)
  3. {
  4.     return utf8_decode($str);
  5.     // alternatively we could have used
  6.     // return mb_convert_encoding($str, 'UTF-8', 'iso-8859-1');
  7. }
  8.  
  9. function utf16ToUTF8($str)
  10. {
  11.     return mb_convert_encoding($str'UTF-8''UTF-16');
  12. }
  13.  
  14. function windows1252ToUTF8($str)
  15. {
  16.     return mb_convert_encoding($str'UTF-8''windows-1252');
  17. }
  18.  
  19. require_once 'XML/Query2XML.php';
  20. require_once 'MDB2.php';
  21. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  22. $dom $query2xml->getXML(
  23.     "SELECT
  24.         *
  25.      FROM
  26.         artist
  27.         LEFT JOIN album ON album.artist_id = artist.artistid",
  28.     array(
  29.         'rootTag' => 'music_library',
  30.         'rowTag' => 'artist',
  31.         'idColumn' => 'artistid',
  32.         'elements' => array(
  33.             'artistid',
  34.             'name'// name is in ISO-8859-1 and therefore will be handled by the default conversion
  35.             'birth_year',
  36.             'birth_place',
  37.             'genre' => array(
  38.                 'value' => 'genre',
  39.                 'encoder' => false  // genre already is in UTF-8
  40.             ),
  41.             'albums' => array(
  42.                 'rootTag' => 'albums',
  43.                 'rowTag' => 'album',
  44.                 'idColumn' => 'albumid',
  45.                 'elements' => array(
  46.                     'albumid',
  47.                     'title' => array(
  48.                         'value' => 'title',
  49.                         'encoder' => 'utf16ToUTF8'  // title is in UTF-16 and therefore needs
  50.                     ),                              // special treatment
  51.                     'published_year'
  52.                 ),
  53.                 'attributes' => array(
  54.                     'comment' => array(
  55.                         'value' => 'comment',
  56.                         'encoder' => 'windows1252ToUTF8'    // comment is in Windows-1252
  57.                     )
  58.                 )
  59.             )
  60.         )
  61.     )
  62. );
  63.  
  64. header('Content-Type: application/xml');
  65.  
  66. $dom->formatOutput true;
  67. print $dom->saveXML();
  68. ?>


Modifying the returned DOMDocument instance

XML_Query2XML::getXML() returns an instance of DOMDocument. I recommend that you do some reading about PHP5's DOM extension.

Let's see how we can add attributes to the root element (i.e. the first child of the DOMDocument instance returned by getXML()):

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5.  
  6. $doc $query2xml->getXML(
  7.     "SELECT
  8.         *
  9.      FROM
  10.         artist",
  11.     array(
  12.         'rootTag' => 'music_library',
  13.         'rowTag' => 'artist',
  14.         'idColumn' => 'artistid',
  15.         'elements' => array(
  16.             'artistid',
  17.             'name',
  18.             'birth_year',
  19.             'birth_place',
  20.             'genre'
  21.         )
  22.     )
  23. );
  24. $root $doc->firstChild;
  25. $root->setAttribute('copyright''John Doe 2007');
  26.  
  27. header('Content-Type: application/xml');
  28. $doc->formatOutput true;
  29. print $doc->saveXML();
  30. ?>

This adds an attribute named 'copyright' with a value of 'John Doe 2007' to the root element <music_library>:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <music_library copyright="John Doe 2007">
  3.   <artist>
  4.     <artistid>1</artistid>
  5.     <name>Curtis Mayfield</name>
  6.     <birth_year>1920</birth_year>
  7.     <birth_place>Chicago</birth_place>
  8.     <genre>Soul</genre>
  9.   </artist>
  10.   <artist>
  11.     <artistid>2</artistid>
  12.     <name>Isaac Hayes</name>
  13.     <birth_year>1942</birth_year>
  14.     <birth_place>Tennessee</birth_place>
  15.     <genre>Soul</genre>
  16.   </artist>
  17.   <artist>
  18.     <artistid>3</artistid>
  19.     <name>Ray Charles</name>
  20.     <birth_year>1930</birth_year>
  21.     <birth_place>Mississippi</birth_place>
  22.     <genre>Country and Soul</genre>
  23.   </artist>
  24. </music_library>

Final Notes on XML_Query2XML::getXML()

You might also want to read the API docs: XML_Query2XML.

Integrating other XML data sources

Introduction

Since release 1.1.0 it is possible to integrate other XML data sources into the XML data that is returned by XML_Query2XML::getXML(). For example you might want to store XML data in your relational database and integrate that into your xml feed. In this case the XML data is only present in serialized form and therefore needs to be unserialized first. This means that the data has to be converted into a DOMDocument using DOMDocument::loadXML(). It might as well be that you already have a PHP application running that creates a DOMDocument. In that case no unserialization is needed.

The unserialization prefix (&)

To unserialize xml data you can use the UNSERIALIZATION prefix &. It has the following characteristics:

  • DOMDocument::loadXML() is used for unserialization.
  • If the data to unserialize is an empty string ('') or null the data will be silently ignored and no XML elements will be created.
  • A XML_Query2XML_XMLException will be thrown if the unserialization fails, i.e. DOMDocument::loadXML() returns false. This will happen for example if the data you try to unserialize is 'John Doe' (no xml tags at all), '<name>John Doe' (no closing tag) or '<name>John Doe</name><name>Jane Doe</name>' (no root tag).

Usage scenarios of the unserialization prefix (&)

Regarding a container (the root element of your unserialized data) there are 3 different things you might want when unserializing the data:

  • Container always present
  • Container only present if there are children
  • No Container
For the detailed description of each of the three possibilities that is to follow, we will assume that your XML data is stored in the database. Therefore & will be used in the form of '&COLUMN_NAME'. If you wanted to unserialize static data you would write something like
  1. '&:<name>John Doe</name>'
or if you wanted to unserialize a string returned from a callback function, you would use
  1. '&#MyClass::myFunction()'
  • Container always present: The container element will be present even if there is no data to be unserialized. This is the default behaviour:
    1. <?php
    2. require_once 'XML/Query2XML.php';
    3. require_once 'MDB2.php';
    4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
    5. $dom =$query2xml->getXML(
    6.     "SELECT
    7.         *, NULL AS additional_xml
    8.      FROM
    9.         store",
    10.     array(
    11.         'rootTag' => 'music_stores',
    12.         'rowTag' => 'store',
    13.         'idColumn' => 'storeid',
    14.         'elements' => array(
    15.             'storeid',
    16.             'country',
    17.             'state',
    18.             'city',
    19.             'street',
    20.             'building_xmldata' => '&building_xmldata',
    21.             'additional_xml' => '&additional_xml',
    22.         )
    23.     )
    24. );
    25. $dom->formatOutput true;
    26. print $dom->saveXML();
    27. ?>
    For both records the 'building_xmldata' column contains a <building> element that has 3 children: <floors>, <elevators> and <square_meters>. But there is always a surrounding <building_xmldata> tag. The 'additional_xml' column is NULL for both records but an empty <additional_xml/> element gets created for both of them.
    1. <?xml version="1.0" encoding="UTF-8"?>
    2. <music_stores>
    3.   <store>
    4.     <storeid>1</storeid>
    5.     <country>US</country>
    6.     <state>New York</state>
    7.     <city>New York</city>
    8.     <street>Broadway &amp; 72nd Str</street>
    9.     <building_xmldata>
    10.       <building>
    11.         <floors>4</floors>
    12.         <elevators>2</elevators>
    13.         <square_meters>3200</square_meters>
    14.       </building>
    15.     </building_xmldata>
    16.     <additional_xml/>
    17.   </store>
    18.   <store>
    19.     <storeid>2</storeid>
    20.     <country>US</country>
    21.     <state>New York</state>
    22.     <city>Larchmont</city>
    23.     <street>Palmer Ave 71</street>
    24.     <building_xmldata>
    25.       <building>
    26.         <floors>2</floors>
    27.         <elevators>1</elevators>
    28.         <square_meters>400</square_meters>
    29.       </building>
    30.     </building_xmldata>
    31.     <additional_xml/>
    32.   </store>
    33. </music_stores>
    Note: you would get exactly the same result by using the 'value' option within a Complex Element Specifications instead of a Simple Element Specifications. Instead of
    1. 'building_xmldata' => '&building_xmldata'
    you would write
    1. 'building_xmldata' => array(
    2.     'value' => '&building_xmldata'
    3. )
  • Container only present if there are children: The container will only be present if the unserialization produces at least one XML element. This is achieved by using the CONDITIONAL prefix (?):
    1. <?php
    2. require_once 'XML/Query2XML.php';
    3. require_once 'MDB2.php';
    4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
    5. $dom =$query2xml->getXML(
    6.     "SELECT
    7.         *, NULL AS additional_xml
    8.      FROM
    9.         store",
    10.     array(
    11.         'rootTag' => 'music_stores',
    12.         'rowTag' => 'store',
    13.         'idColumn' => 'storeid',
    14.         'elements' => array(
    15.             'storeid',
    16.             'country',
    17.             'state',
    18.             'city',
    19.             'street',
    20.             'building_xmldata' => '?&building_xmldata',
    21.             'additional_xml' => '?&additional_xml',
    22.         )
    23.     )
    24. );
    25. $dom->formatOutput true;
    26. print $dom->saveXML();
    27. ?>
    The resulting XML data shows that the unserialized XML is still enclosed by a <building_xmldata> tag but the <additional_xml> elements are gone:
    1. <?xml version="1.0" encoding="UTF-8"?>
    2. <music_stores>
    3.   <store>
    4.     <storeid>1</storeid>
    5.     <country>US</country>
    6.     <state>New York</state>
    7.     <city>New York</city>
    8.     <street>Broadway &amp; 72nd Str</street>
    9.     <building_xmldata>
    10.       <building>
    11.         <floors>4</floors>
    12.         <elevators>2</elevators>
    13.         <square_meters>3200</square_meters>
    14.       </building>
    15.     </building_xmldata>
    16.   </store>
    17.   <store>
    18.     <storeid>2</storeid>
    19.     <country>US</country>
    20.     <state>New York</state>
    21.     <city>Larchmont</city>
    22.     <street>Palmer Ave 71</street>
    23.     <building_xmldata>
    24.       <building>
    25.         <floors>2</floors>
    26.         <elevators>1</elevators>
    27.         <square_meters>400</square_meters>
    28.       </building>
    29.     </building_xmldata>
    30.   </store>
    31. </music_stores>
    Again: the same results can be achieved by using the 'value' option within a Complex Element Specifications instead of a Simple Element Specifications. Instead of
    1. 'building_xmldata' => '?&building_xmldata'
    you would write
    1. 'building_xmldata' => array(
    2.     'value' => '?&building_xmldata'
    3. )
  • No Container: Even if the unserialization produces an XML element no container will be used. You have to effectively hide the container by using the hidden_container_prefix that can be set using XML_Query2XML::setGlobalOption() and defaults to '__'. Here goes an example:
    1. <?php
    2. require_once 'XML/Query2XML.php';
    3. require_once 'MDB2.php';
    4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
    5. $dom =$query2xml->getXML(
    6.     "SELECT
    7.         *
    8.      FROM
    9.         store",
    10.     array(
    11.         'rootTag' => 'music_stores',
    12.         'rowTag' => 'store',
    13.         'idColumn' => 'storeid',
    14.         'elements' => array(
    15.             'storeid',
    16.             'country',
    17.             'state',
    18.             'city',
    19.             'street',
    20.             '__building_xmldata' => '&building_xmldata'
    21.         )
    22.     )
    23. );
    24. $dom->formatOutput true;
    25. print $dom->saveXML();
    26. ?>
    The resulting XML now does not contain <building_xmldata> tags that surround the <building> elements:
    1. <?xml version="1.0" encoding="UTF-8"?>
    2. <music_stores>
    3.   <store>
    4.     <storeid>1</storeid>
    5.     <country>US</country>
    6.     <state>New York</state>
    7.     <city>New York</city>
    8.     <street>Broadway &amp; 72nd Str</street>
    9.     <building>
    10.       <floors>4</floors>
    11.       <elevators>2</elevators>
    12.       <square_meters>3200</square_meters>
    13.     </building>
    14.   </store>
    15.   <store>
    16.     <storeid>2</storeid>
    17.     <country>US</country>
    18.     <state>New York</state>
    19.     <city>Larchmont</city>
    20.     <street>Palmer Ave 71</street>
    21.     <building>
    22.       <floors>2</floors>
    23.       <elevators>1</elevators>
    24.       <square_meters>400</square_meters>
    25.     </building>
    26.   </store>
    27. </music_stores>
    The same results can be achieved by using the 'value' option within a Complex Element Specifications instead of a Simple Element Specifications. Instead of
    1. '__building_xmldata' => '&building_xmldata'
    you would write
    1. '__building_xmldata' => array(
    2.     'value' => '&building_xmldata'
    3. )
    or (which is effectively the same)
    1. 'building_xmldata' => array(
    2.     'rowTag' => '__building_xmldata'
    3.     'value' => '&building_xmldata'
    4. )
    Please also note that using the CONDITIONAL prefix (?) in conjunction with the hidden_container_prefix '__' does not change the resulting XML data in any way:
    1. '__building_xmldata' => '?&building_xmldata'


Writing your own unserialization method

If you are unhappy with these chracteristics (e.g. you want invalid XML data to be ignored rather than causing an exception to be thrown) you could do your own unserialization using a CALLBACK FUNCTION (i.e. the # prefix). Here is what the unserialization performed by the & prefix looks like

  1. if (strlen($xmlData)) {
  2.     $doc new DOMDocument();
  3.     if (!@$doc->loadXML($xmlData)) {
  4.         throw new XML_Query2XML_XMLException(
  5.             'Could not unserialize the following XML data: '
  6.             . $ret
  7.         );
  8.     }
  9.     return $doc->documentElement;
  10. else {
  11.     return null;
  12. }


Returning DOMNode instances from callbacks

If you want to integrate XML_Query2XML with another PHP application that uses PHP5's DOM, you can make your callbacks return an instance of DOMNode or an array of DOMNode instances.

In the following example we return a <unixtime> tag from the callback function getTime(). It will be placed inside a <time> tag.

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom =$query2xml->getXML(
  6.     "SELECT
  7.         *
  8.      FROM
  9.         album",
  10.     array(
  11.         'rootTag' => 'music_store',
  12.         'rowTag' => 'album',
  13.         'idColumn' => 'albumid',
  14.         'elements' => array(
  15.             'albumid',
  16.             'title',
  17.             'time' => '#getTime()'
  18.         )
  19.     )
  20. );
  21. $dom->formatOutput true;
  22. print $dom->saveXML();
  23.  
  24. function getTime()
  25. {
  26.     $dom new DOMDocument();
  27.     $unixtime $dom->createElement('unixtime');
  28.     $unixtime->appendChild($dom->createTextNode(time()));
  29.     return $unixtime;
  30. }
  31. ?>
Have a look at the resulting XML data:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <music_store>
  3.   <album>
  4.     <albumid>1</albumid>
  5.     <title>New World Order</title>
  6.     <time>
  7.       <unixtime>1167167461</unixtime>
  8.     </time>
  9.   </album>
  10.   <album>
  11.     <albumid>2</albumid>
  12.     <title>Curtis</title>
  13.     <time>
  14.       <unixtime>1167167461</unixtime>
  15.     </time>
  16.   </album>
  17.   <album>
  18.     <albumid>3</albumid>
  19.     <title>Shaft</title>
  20.     <time>
  21.       <unixtime>1167167461</unixtime>
  22.     </time>
  23.   </album>
  24. </music_store>
Now we modify the example so that getTime() returns multiple DOMNode instances in an array. We will also "hide" the surrounding element using the hidden_container_prefix that can be set using XML_Query2XML::setGlobalOption() and defaults to '__':
  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom =$query2xml->getXML(
  6.     "SELECT
  7.         *
  8.      FROM
  9.         album",
  10.     array(
  11.         'rootTag' => 'music_store',
  12.         'rowTag' => 'album',
  13.         'idColumn' => 'albumid',
  14.         'elements' => array(
  15.             'albumid',
  16.             'title',
  17.             '__time' => '#getTime()'
  18.         )
  19.     )
  20. );
  21. $dom->formatOutput true;
  22. print $dom->saveXML();
  23.  
  24. function getTime()
  25. {
  26.     $dom new DOMDocument();
  27.     $unixtime $dom->createElement('unixtime');
  28.     $unixtime->appendChild($dom->createTextNode(time()));
  29.     
  30.     $rfc2822date $dom->createElement('rfc2822date');
  31.     $rfc2822date->appendChild($dom->createTextNode(date('r')));
  32.     return array($unixtime$rfc2822date);
  33. }
  34. ?>
The surrounding <time> element is now gone and we have both tags <unixtime> and <rfc2822date>:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <music_store>
  3.   <album>
  4.     <albumid>1</albumid>
  5.     <title>New World Order</title>
  6.     <unixtime>1167169325</unixtime>
  7.     <rfc2822date>Tue, 26 Dec 2006 22:42:05 +0100</rfc2822date>
  8.   </album>
  9.   <album>
  10.     <albumid>2</albumid>
  11.     <title>Curtis</title>
  12.     <unixtime>1167169325</unixtime>
  13.     <rfc2822date>Tue, 26 Dec 2006 22:42:05 +0100</rfc2822date>
  14.   </album>
  15.   <album>
  16.     <albumid>3</albumid>
  17.     <title>Shaft</title>
  18.     <unixtime>1167169325</unixtime>
  19.     <rfc2822date>Tue, 26 Dec 2006 22:42:05 +0100</rfc2822date>
  20.   </album>
  21. </music_store>

Exception Handling

The public methods XML_Query2XML::factory(), XML_Query2XML::getFlatXML() and XML_Query2XML::getXML() all may throw exceptions. For production use you will have to implement the security principle "secure failure". This means that you will have to catch exceptions and deal with them. XML_Query2XML makes this task easy as all exceptions this package will ever throw extend XML_Query2XML_Exception. Therefore it is possible to catch all exceptions by catching XML_Query2XML_Exception:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. try {
  4.     require_once 'MDB2.php';
  5.     $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  6.     $dom $query2xml->getXML(
  7.       "SELECT * FROM artist",
  8.       array(
  9.         'rootTag' => 'favorite_artist',
  10.         'idColumn' => 'artistid',
  11.         'rowTag' => 'artist',
  12.         'elements' => array(
  13.             'name',
  14.             'birth_year',
  15.             'genre'
  16.         )
  17.       )
  18.     );
  19.     echo $dom->saveXML();
  20. catch(XML_Query2XML_Exception $e{
  21.     /*
  22.     * log this exceptions
  23.     * display some error message that does not disclose sensitive information
  24.     */
  25. }
  26. ?>

Here is a list of the exceptions the public methods of XML_Query2XML will throw:

As you can see, XML_Query2XML_Exception itself is never thrown.

To treat different exceptions differently you would write code like this:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. try {
  4.     require_once 'MDB2.php';
  5.     $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  6.     $dom $query2xml->getXML(
  7.       "SELECT * FROM artist",
  8.       array(
  9.         'rootTag' => 'favorite_artist',
  10.         'idColumn' => 'artistid',
  11.         'rowTag' => 'artist',
  12.         'elements' => array(
  13.             'name',
  14.             'birth_year',
  15.             'genre'
  16.         )
  17.       )
  18.     );
  19.     echo $dom->saveXML();
  20. catch(XML_Query2XML_DBException $e{
  21.     //handle DB error
  22.     //handle XML error
  23. catch(XML_Query2XML_Exception $e{
  24.     /*
  25.     * Handle all other errors/exceptions; this will not only catch
  26.     * XML_Query2XML_ConfigException but also all other exceptions that might be
  27.     * added in future releases of XML_Query2XML.
  28.     */
  29. }
  30. ?>
Bottom line: make sure you at least have a catch block for XML_Query2XML_Exception.

Output formatting

Before calling the saveXML() method on your DOMDocument instance set its public property formatOutput to true! Here goes an example:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.   "SELECT * FROM artist",
  7.   array(
  8.     'rootTag' => 'favorite_artist',
  9.     'idColumn' => 'artistid',
  10.     'rowTag' => 'artist',
  11.     'elements' => array(
  12.         'name',
  13.         'birth_year',
  14.         'genre'
  15.     )
  16.   )
  17. );
  18. header('Content-Type: application/xml');
  19.  
  20. $dom->formatOutput true;
  21. print $dom->saveXML();
  22. ?>

Alternatively you could also use PEAR XML_Beautifier. Here goes an example:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.   "SELECT * FROM artist",
  7.   array(
  8.     'rootTag' => 'favorite_artist',
  9.     'idColumn' => 'artistid',
  10.     'rowTag' => 'artist',
  11.     'elements' => array(
  12.         'name',
  13.         'birth_year',
  14.         'genre'
  15.     )
  16.   )
  17. );
  18. header('Content-Type: application/xml');
  19. print '<?xml version="1.0" encoding="UTF-8"?>' "\n";
  20.  
  21. require_once 'XML/Beautifier.php';
  22. $beautifier new XML_Beautifier();
  23. print $beautifier->formatString($dom->saveXML());
  24. ?>

Output Caching

If your XML data is rather static in nature, i.e. exactely the same XML data is created over and over again you might want to use some kind of output caching. I will demonstrate the usage of PEAR Cache_Lite here:

  1. <?php
  2. require_once('Cache/Lite.php');
  3. /*
  4. * Set a id for this cache; if you generate the XML data based
  5. * on values passed via POST/GET/COOKIE, include these values
  6. * in $id. This does not need to be an integer; it's md5sum
  7. * will be used.
  8. */
  9. $id '123';
  10.  
  11. //set a few options
  12. $options array(
  13.     'cacheDir' => "/tmp/",
  14.     'lifeTime' => 3600
  15. );
  16.  
  17. //create a Cache_Lite object
  18. $cache new Cache_Lite($options);
  19.  
  20. // content type for xml data
  21. header('Content-Type: application/xml');
  22.  
  23. //test if there is a valide cache for this id
  24. if ($data $cache->get($id)) {
  25.     print $data;
  26. else {
  27.     //no valid cache found
  28.     require_once 'XML/Query2XML.php';
  29.     require_once 'MDB2.php';
  30.     $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  31.     $dom $query2xml->getXML(...);
  32.     $dom->formatOutput true;
  33.     
  34.     //cache the XML data
  35.     $data $dom->saveXML();
  36.     $cache->save($data$id);
  37.     
  38.     print $data;
  39. }
  40. ?>
For more information on PEAR Cache_Lite, please see the Cache_Lite manual.

XML encoding

It is highly recommended to use UTF-8 for your XML data. But if you want to use another encoding standard you have to:

  • Use $options['encoder'] to convert all node and attribute values to the desired encoding standard. If source and destination encoding standards are the same, you just have to set $options['encoder'] to false.
  • Set the encoding property of the DOMDocument instance returned by XML_Query2XML::getXML().

For some introduction to XML encoding please see http://www.w3schools.com/xml/xml_encoding.asp and http://www.opentag.com/xfaq_enc.htm. Note: I highly recommend to use UTF-8 for XML if you don't have a compelling reason to use an other encoding standard.

Here goes an example that shows how to use ISO-8859-1 (Latin-1) for XML encoding. We will assume that your data is in ISO-8859-1 and therefore does not need any conversion.

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.     "SELECT
  7.         *
  8.      FROM
  9.         artist",
  10.     array(
  11.         'rootTag' => 'music_library',
  12.         'rowTag' => 'artist',
  13.         'idColumn' => 'artistid',
  14.         'encoder' => false,     // disable default ISO-8859-1 to UTF-8 encoding
  15.         'elements' => array(
  16.             'artistid',
  17.             'name',
  18.             'birth_year',
  19.             'birth_place',
  20.             'genre'
  21.         )
  22.     )
  23. );
  24.  
  25. header('Content-Type: application/xml');
  26. $dom->formatOutput true;
  27.  
  28. $dom->encoding 'iso-8859-1';  //setting XML encoding
  29.  
  30. print $dom->saveXML();
  31. ?>
This results in the following XML:
  1. <?xml version="1.0" encoding="iso-8859-1"?>
  2. <music_library>
  3.   <artist>
  4.     <artistid>1</artistid>
  5.     <name>Curtis Mayfield</name>
  6.     <birth_year>1920</birth_year>
  7.     <birth_place>Chicago</birth_place>
  8.     <genre>Soul</genre>
  9.   </artist>
  10.   <artist>
  11.     <artistid>2</artistid>
  12.     <name>Isaac Hayes</name>
  13.     <birth_year>1942</birth_year>
  14.     <birth_place>Tennessee</birth_place>
  15.     <genre>Soul</genre>
  16.   </artist>
  17.   <artist>
  18.     <artistid>3</artistid>
  19.     <name>Ray Charles</name>
  20.     <birth_year>1930</birth_year>
  21.     <birth_place>Mississippi</birth_place>
  22.     <genre>Country and Soul</genre>
  23.   </artist>
  24. </music_library>

Handling Binary Data

If you want to include binary data (e.g. JPEG data) in XML data you should encode your binary data to make sure that it does not include a sequence of bytes that represent the characters "</" or a null byte (which usually denotes the end of a string). The most common binary data encoding for XML is base64. The most straightforward way to do base64 encoding for an element or attribute value is to use the BASE64 ENCODING shortcut '^'.

In the following example we will assume that the column album.comment contains binary data:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4.  
  5. $mdb2 MDB2::factory('mysql://root@localhost/Query2XML_Tests');
  6. $query2xml XML_Query2XML::factory($mdb2);
  7.  
  8. $dom $query2xml->getXML(
  9.     'SELECT * FROM album',
  10.     array(
  11.         'idColumn' => 'albumid',
  12.         'rowTag' => 'album',
  13.         'rootTag' => 'music_store',
  14.         'elements' => array(
  15.             'albumid',
  16.             'title',
  17.             'comment' => '^comment'
  18.         )
  19.     )
  20. );
  21.  
  22. header('Content-Type: application/xml');
  23. $dom->formatOutput true;
  24. print $dom->saveXML();
  25. ?>
The resulting XML data looks like this:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <music_store>
  3.   <album>
  4.     <albumid>1</albumid>
  5.     <title>New World Order</title>
  6.     <comment>dGhlIGJlc3QgZXZlciE=</comment>
  7.   </album>
  8.   <album>
  9.     <albumid>2</albumid>
  10.     <title>Curtis</title>
  11.     <comment>dGhhdCBtYW4ncyBnb3Qgc29tdGhpbicgdG8gc2F5</comment>
  12.   </album>
  13.   <album>
  14.     <albumid>3</albumid>
  15.     <title>Shaft</title>
  16.     <comment>aGUncyB0aGUgbWFu</comment>
  17.   </album>
  18. </music_store>

Using dynamic $options to dump all data of your database

For some reason you might simply want to dump every table in your database with all their records. If you don't want to go over your code everytime a new table was added, you need to generate (parts of) the $options argument on the fly. Here is one way to do it:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4.  
  5. $mdb2 MDB2::factory('mysql://root@localhost/Query2XML_Tests');
  6. $query2xml XML_Query2XML::factory($mdb2);
  7.  
  8.  
  9. //we need MDB2's manager module to get the list of tables in a database independent way
  10. $mdb2->loadModule('Manager');
  11. $elements array();
  12. $mdb2->setOption('portability'MDB2_PORTABILITY_NONE);
  13. $tables $mdb2->listTables();
  14. $mdb2->setOption('portability'MDB2_PORTABILITY_ALL);
  15. for ($i 0$i count($tables)$i++{
  16.     $elements['table' $iarray(
  17.         'rowTag' => 'table',
  18.         'attributes' => array(
  19.             'name' => ':' $tables[$i]
  20.         ),
  21.         'elements' => array(
  22.             'record' => array(
  23.                 'idColumn' => false,
  24.                 'sql' => 'SELECT * FROM ' $tables[$i],
  25.                 'elements' => array(
  26.                     '*'
  27.                 )
  28.             )
  29.         )
  30.     );
  31. }
  32.  
  33. $dom $query2xml->getXML(
  34.     false,
  35.     array(
  36.         'idColumn' => false,
  37.         'rowTag' => '__tables',
  38.         'rootTag' => 'database',
  39.         'elements' => $elements
  40.     )
  41. );
  42.  
  43. header('Content-Type: application/xml');
  44. $dom->formatOutput true;
  45. print $dom->saveXML();
  46. ?>

Notice how we used MDB2's manager module to get a list of all tables in our database. We then loop over the names and create $options['elements'] at run time.

Side note: unfortunately MDB2's portability feature per default makes listTabes() return all table names lower-cased. To circumvent this we have to temporarily change the portability option. See http://pear.php.net/bugs/bug.php?id=11215 which describes the issue in greater detail.

Each table tag will have an attribute "name". We use the ':' prefix to indicate that what follows is a static text not to be interpreted as a column name.

When looking at the getXML() call, you'll notice that we didn't pass a query as the first argument ($sql) but rather a boolean value of false. As documented at $sql this will make XML_Query2XML behave as if we used a query that returned a single record with no columns. This is necessary because in our example we use multiple unrelated queries that we simply want to palce inside a database tag.

Also notice that we use the boolean value of FALSE for $options['idColumn'] at the root level and for each of the tables. It is OK to do so on the root level because we are actually only dealing with a single fake record there. To use FALSE for $options['idColumn'] on the sub-levels within the $elements array generated at run-time is also OK because we will always want all records and (which is the reason why we are not violating best practices) we simply don't know the primary key columns for all tables that might get created in the future.

Also note that we used '__tables' for $options['rowTag'] at the root level: this is because we don't have anything to loop over at the root level - remember using false for $sql is like using a query that returns a single record with no columns.

Working without Shortcuts and Prefixes

You think having to deal with all these different prefixes (e.g. #, ?, :, &, ^ and =) when defining $options['value'] and Simple Element Specifications just makes things more complicated or your code less readable? In that case, just don't use them! Below is way of getting almost the same functionality with a command object pattern implementation. If you have to deal with some 10,000+ records in your XML, using command objects might even improve performance a little bit (~1-3%).

The following example first provides command classes that implement the functionality of the currently available prefixes.

  1. <?php
  2. /**All command objects have to implement the interface XML_Query2XML_Callback.
  3. */
  4. require_once 'XML/Query2XML/Callback.php';
  5.  
  6. /**All command classes that work on a single column value will extend this abstract class.
  7. * Classes extending this class will have to implement the execute(array $record) method
  8. * defined by the XML_Query2XML_Callback interface.
  9. */
  10. abstract class Callback_SingleColumn implements XML_Query2XML_Callback
  11. {
  12.     /**The column name
  13.     * @var string
  14.     */
  15.     protected $_column '';
  16.     
  17.     /**Constructor
  18.     * @param string $column The name of the column this instance will work on.
  19.     */
  20.     public function __construct($column)
  21.     {
  22.         $this->_column $column;
  23.     }
  24.     
  25.     /**Get the value of the column passed to the constructor.
  26.     * @param array $record An associative array as it will be passed
  27.     *                      to the execute(array $record) method.
  28.     * @throws XML_Query2XML_Exception If the column name passed
  29.     *                      to the constructor was not found in $record.
  30.     */
  31.     protected function _getColumnValue(array $record)
  32.     {
  33.         if (array_key_exists($this->_column$record)) {
  34.             return $record[$this->_column];
  35.         }
  36.         throw new XML_Query2XML_Exception(
  37.             'Column ' $this->_column ' was not found in the result set'
  38.         );
  39.     }
  40. }
  41.  
  42. /**Use an instance of this class to get the base64 encoded value of a column.
  43. */
  44. class Callback_Base64 extends Callback_SingleColumn
  45. {
  46.     /**Called by XML_Query2XML for every record.
  47.     * @param array $record An associative array.
  48.     * @return string
  49.     */
  50.     public function execute(array $record)
  51.     {
  52.         return base64_encode($this->_getColumnValue($record));
  53.     }
  54. }
  55.  
  56. /**Use an instance of this class to unserialize XML data stored in a column.
  57. */
  58. class Callback_Unserialization extends Callback_SingleColumn
  59. {
  60.     /**Called by XML_Query2XML for every record.
  61.     * @param array $record An associative array.
  62.     * @return DOMElement
  63.     * @throws XML_Query2XML_XMLException If unserialization fails.
  64.     */
  65.     public function execute(array $record)
  66.     {
  67.         $doc new DOMDocument();
  68.         $xml $this->_getColumnValue($record);
  69.         if (!@$doc->loadXML($xml)) {
  70.             throw new XML_Query2XML_XMLException(
  71.                 'Could not unserialize the following XML data: '
  72.                 . $xml
  73.             );
  74.         }
  75.         return $doc->documentElement;
  76.     }
  77. }
  78.  
  79. /**Use an instance of this class to place a CDATA section around the value of a column.
  80. */
  81. class Callback_CDATA extends Callback_SingleColumn
  82. {
  83.     /**Called by XML_Query2XML for every record.
  84.     * @param array $record An associative array.
  85.     * @return DOMCDATASection 
  86.     */
  87.     public function execute(array $record)
  88.     {
  89.         $doc new DOMDocument();
  90.         return $doc->createCDATASection($this->_getColumnValue($record));
  91.     }
  92. }
  93.  
  94. /**Use an instance of this class to return a static data.
  95. */
  96. class Callback_StaticData implements XML_Query2XML_Callback
  97. {
  98.     /**The static data
  99.     * @var mixed
  100.     */
  101.     private $_data null;
  102.     
  103.     /**Constructor
  104.     * @param mixed $data The static date to return for every record.
  105.     */
  106.     public function __construct($data)
  107.     {
  108.         $this->_data $data;
  109.     }
  110.     
  111.     /**Called by XML_Query2XML for every record.
  112.     * This method will always return the same data, no matter what
  113.     * is passed as $record.
  114.     *
  115.     * @param array $record An associative array.
  116.     * @return mixed 
  117.     */
  118.     public function execute(array $record)
  119.     {
  120.         return $this->_data;
  121.     }
  122. }
  123.  
  124. require_once 'XML/Query2XML.php';
  125. require_once 'MDB2.php';
  126.  
  127. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  128.  
  129. $xml $query2xml->getXML(
  130.     'SELECT * FROM store',
  131.     array(
  132.         'rootTag' => 'stores',
  133.         'rowTag' => 'store',
  134.         'idColumn' => 'storeid',
  135.         'elements' => array(
  136.             'base64' => new Callback_Base64('building_xmldata'),
  137.             'cdata' => new Callback_CDATA('building_xmldata'),
  138.             'static' => new Callback_StaticData('my static data'),
  139.             'unserialized' => new Callback_Unserialization('building_xmldata')
  140.         )
  141.     )
  142. );
  143.  
  144. $xml->formatOutput true;
  145. print $xml->saveXML();
  146. ?>
Note that the above code is equivalent to
  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4.  
  5. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  6.  
  7. $xml $query2xml->getXML(
  8.     'SELECT * FROM store',
  9.     array(
  10.         'rootTag' => 'stores',
  11.         'rowTag' => 'store',
  12.         'idColumn' => 'storeid',
  13.         'elements' => array(
  14.             'base64' => '?^building_xmldata',
  15.             'cdata' => '=building_xmldata',
  16.             'static' => ':my static data',
  17.             'unserialized' => '&building_xmldata'
  18.         )
  19.     )
  20. );
  21.  
  22. $xml->formatOutput true;
  23. print $xml->saveXML();
  24. ?>
Both produce the following XML data:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <stores>
  3.   <store>
  4.     <base64>PGJ1aWxkaW5nPjxmbG9vcnM+NDwvZmxvb3JzPjxlbGV2YXRvcnM+MjwvZWxldmF0b3JzPjxzcXVhcmVfbWV0ZXJzPjMyMDA8L3NxdWFyZV9tZXRlcnM+PC9idWlsZGluZz4=</base64>
  5.     <cdata>< ![CDATA[<building><floors>4</floors><elevators>2</elevators><square_meters>3200</square_meters></building>]] ></cdata>
  6.     <static>my static data</static>
  7.     <unserialized>
  8.       <building>
  9.         <floors>4</floors>
  10.         <elevators>2</elevators>
  11.         <square_meters>3200</square_meters>
  12.       </building>
  13.     </unserialized>
  14.   </store>
  15.   <store>
  16.     <base64>PGJ1aWxkaW5nPjxmbG9vcnM+MjwvZmxvb3JzPjxlbGV2YXRvcnM+MTwvZWxldmF0b3JzPjxzcXVhcmVfbWV0ZXJzPjQwMDwvc3F1YXJlX21ldGVycz48L2J1aWxkaW5nPg==</base64>
  17.     <cdata>< ![CDATA[<building><floors>2</floors><elevators>1</elevators><square_meters>400</square_meters></building>]] ></cdata>
  18.     <static>my static data</static>
  19.     <unserialized>
  20.       <building>
  21.         <floors>2</floors>
  22.         <elevators>1</elevators>
  23.         <square_meters>400</square_meters>
  24.       </building>
  25.     </unserialized>
  26.   </store>
  27. </stores>

Finally, note that you may also explecitely unregister a specific prefix using XML_Query2XML::unregisterPrefix() are unregister all previously defined prefixes using XML_Query2XML::unregisterAllPrefixes().

Defining your own Prefixes

You may want to define your own prefixes in order to have a simple and quick way to invoke complex functionality you need often. The registration of the prefix is very much straight forward:

XML_Query2XML::registerPrefix($prefix, $className, $filePath = '')

Note that you may also register a prefix that has been registered before (or is registered by default). In that case, the new registration will simply overwrite the old one.

More difficult than simply registering the prefix, is writing the class that will implement its functionality. All such classes will have to implement the interface XML_Query2XML_Data. However, in practice you will want to extend one of the following abstract classes, depending on the type of functionality you need:

  • XML_Query2XML_Data_Processor: extend this class if you want to use any data as input other than the string specified right after the prefix. For example, if you want to use a column value as input data, extend this class and let the built-in code handle the rest. (Note: the BASE64 ENCODING prefix ^, CDATA SECTION prefix =, and the XML UNSERIALIZATION prefix & are built using this class.)
  • XML_Query2XML_Data_Source: extend this class if you only want to use the string specified right after the prefix as your data source. (Note: the STATIC TEXT prefix : and the CALLBACK FUNCTION prefix # are built using this class.)
  • XML_Query2XML_Data_Condition: extend this class if you want to implement some kind of condition that will determine whether the XML element for which the value was to be used will be included in the resulting output. (Note: the CONDITIONAL prefix ? is built using this class.)

Now, let's suppose you want to use the prefix "" for your command class Year2UnixTime which calculates the Unix time (seconds since the Unix Epoch, January 1 1970 00:00:00 GMT) from a given year. In such a case you clearly want to extend XML_Query2XML_Data_Processor:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'XML/Query2XML/Data/Processor.php';
  4. require_once 'MDB2.php';
  5.  
  6. class Year2UnixTime extends XML_Query2XML_Data_Processor
  7. {
  8.     /**
  9.      * Create a new instance of this class.
  10.      *
  11.      * @param mixed  $preProcessor The pre-processor to be used. An instance of
  12.      *                              XML_Query2XML_Data or null.
  13.      * @param string $configPath   The configuration path within the $options
  14.      *                              array.
  15.      *
  16.      * @return XML_Query2XML_Data_Processor_Base64 
  17.      */
  18.     public function create($preProcessor$configPath)
  19.     {
  20.         $processor new Year2UnixTime($preProcessor);
  21.         // The create() method of every class extending
  22.         // XML_Query2XML_Data_Processor has to call
  23.         // setConfigPath() manually!
  24.         $processor->setConfigPath($configPath);
  25.         return $processor;
  26.     }
  27.     
  28.     /**
  29.      * Called by XML_Query2XML for every record in the result set.
  30.      *
  31.      * @param array $record An associative array.
  32.      *
  33.      * @return string The base64-encoded version the string returned
  34.      *                 by the pre-processor.
  35.      * @throws XML_Query2XML_ConfigException If the pre-processor returns
  36.      *                 something that cannot be converted to a string
  37.      *                 (i.e. an object or an array).
  38.      */
  39.     public function execute(array $record)
  40.     {
  41.         $data $this->runPreProcessor($record);
  42.         if (is_array($data|| is_object($data)) {
  43.             throw new XML_Query2XML_ConfigException(
  44.                 $this->getConfigPath()
  45.                 . ': XML_Query2XML_Data_Processor_Base64: string '
  46.                 . 'expected from pre-processor, but ' gettype($data' returned.'
  47.             );
  48.         }
  49.         return DateTime::createFromFormat('Y-m-d H:i:s'$data '-01-01 00:00:00'new DateTimeZone('Etc/GMT+0'))->format('U');
  50.     }
  51. }
  52.  
  53. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  54. $query2xml->registerPrefix('''Year2UnixTime');
  55. $dom $query2xml->getXML(
  56.   "SELECT * FROM artist",
  57.   array(
  58.     'rootTag' => 'artists',
  59.     'idColumn' => 'artistid',
  60.     'rowTag' => 'artist',
  61.     'elements' => array(
  62.         'name',
  63.         'birth_year',
  64.         'birth_year_in_unix_time' => 'birth_year'
  65.     )
  66.   )
  67. );
  68.  
  69. header('Content-Type: application/xml');
  70. $dom->formatOutput true;
  71. print $dom->saveXML();
  72. ?>
The resulting XML data looks like this:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <artists>
  3.   <artist>
  4.     <name>Curtis Mayfield</name>
  5.     <birth_year>1920</birth_year>
  6.     <birth_year_in_unix_time>-1577923200</birth_year_in_unix_time>
  7.   </artist>
  8.   <artist>
  9.     <name>Isaac Hayes</name>
  10.     <birth_year>1942</birth_year>
  11.     <birth_year_in_unix_time>-883612800</birth_year_in_unix_time>
  12.   </artist>
  13.   <artist>
  14.     <name>Ray Charles</name>
  15.     <birth_year>1930</birth_year>
  16.     <birth_year_in_unix_time>-1262304000</birth_year_in_unix_time>
  17.   </artist>
  18. </artists>

Global Options

Global options can be set using the public method XML_Query2XML::setGlobalOption() and retrieved using XML_Query2XML::getGlobalOption(). Currently the following global options are available:

XML_Query2XML::setGlobalOption()

XML_Query2XML::setGlobalOption($option, $value)

Currently there is only one global option: hidden_container_prefix which has to be set to a non empty string. If you try to set a non existing global option or try to set an existing one to an invalid value a XML_Query2XML_ConfigException will be thrown.

Here goes an example:

  1. $query2xml =XML_Query2XML::factory($db);
  2. $query2xml->setGlobalOption('hidden_container_prefix''___');

XML_Query2XML::getGlobalOption()

XML_Query2XML::getGlobalOption($option)

Currently there is only one global option: hidden_container_prefix. Use getGlobalOption() to retrieve a global option's current value:

  1. $query2xml =XML_Query2XML::factory($db);
  2. echo $query2xml->getGlobalOption('hidden_container_prefix');
If you try to retrieve the value of a non existing global option a XML_Query2XML_ConfigException will be thrown.

hidden_container_prefix

All elements whose name start with the string specified in the hidden_container_prefix option are stripped from the DOMDocument that is finally returned by XML_Query2XML::getXML(). The container's child elements are effectively replacing their parent. The default value of the hidden_container_prefix option is '__'. Here is an example using the default:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom =$query2xml->getXML(
  6.     "SELECT
  7.         *
  8.      FROM
  9.         album",
  10.     array(
  11.         'rootTag' => 'music_store',
  12.         'rowTag' => 'album',
  13.         'idColumn' => 'albumid',
  14.         'elements' => array(
  15.             'albumid',
  16.             'title',
  17.             '__info' => '&:<name>John Doe</name>'
  18.         )
  19.     )
  20. );
  21. $dom->formatOutput true;
  22. print $dom->saveXML();
  23. ?>
Instead of
  1. '__info' => '&:<name>John Doe</name>'
we also could have written
  1. 'name' => ':John Doe'
Both versions produce the same XML data. Note how the contents of the <__info> element effectively replaced the <__info> tag itself. It results in the <name> element being directly placed inside the <album> element.
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <music_store>
  3.   <album>
  4.     <albumid>1</albumid>
  5.     <title>New World Order</title>
  6.     <name>John Doe</name>
  7.   </album>
  8.   <album>
  9.     <albumid>2</albumid>
  10.     <title>Curtis</title>
  11.     <name>John Doe</name>
  12.   </album>
  13.   <album>
  14.     <albumid>3</albumid>
  15.     <title>Shaft</title>
  16.     <name>John Doe</name>
  17.   </album>
  18. </music_store>

Now imagine that we actually do want an XML element to be named '__info'. This means that we have to change the hidden_container_prefix option:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $query2xml->setGlobalOption('hidden_container_prefix''___');
  6. $dom =$query2xml->getXML(
  7.     "SELECT
  8.         *
  9.      FROM
  10.         album",
  11.     array(
  12.         'rootTag' => 'music_store',
  13.         'rowTag' => 'album',
  14.         'idColumn' => 'albumid',
  15.         'elements' => array(
  16.             'albumid',
  17.             'title',
  18.             '__info' => '&:<name>John Doe</name>'//will not be hidden
  19.             '___info' => '&:<name>John Doe</name>' //will be hidden
  20.         )
  21.     )
  22. );
  23. $dom->formatOutput true;
  24. print $dom->saveXML();
  25. ?>
This produces the following XML data:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <music_store>
  3.   <album>
  4.     <albumid>1</albumid>
  5.     <title>New World Order</title>
  6.     <__info>
  7.       <name>John Doe</name>
  8.     </__info>
  9.     <name>John Doe</name>
  10.   </album>
  11.   <album>
  12.     <albumid>2</albumid>
  13.     <title>Curtis</title>
  14.     <__info>
  15.       <name>John Doe</name>
  16.     </__info>
  17.     <name>John Doe</name>
  18.   </album>
  19.   <album>
  20.     <albumid>3</albumid>
  21.     <title>Shaft</title>
  22.     <__info>
  23.       <name>John Doe</name>
  24.     </__info>
  25.     <name>John Doe</name>
  26.   </album>
  27. </music_store>

Logging and Debugging XML_Query2XML

If you need to debug your XML_Query2XML or an application using it, use XML_Query2XML::enableDebugLog() and XML_Query2XML::disableDebugLog(). It is recommended to use PEAR Log.

The following information is logged:

  • the beginning of the execution of a SQL query in the database: this includes the SQL statement itself followed by the values used when executing a prepared statement.
  • the end of the execution of a SQL query in the database
  • caching of a query's result
  • retrieving previously cached data
When using PEAR::Log, the date, time, a custom string and '[info]' will preceed every entry.

Here is how it's done:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. $query2xml XML_Query2XML::factory($db);
  4.  
  5. //create a new instance of PEAR::Log
  6. require_once 'Log.php';
  7. $debugLogger Log::factory('file''debug.log''XML_Query2XML');
  8.  
  9. //start debugging
  10. $query2xml->enableDebugLog($debugLogger);
  11.  
  12. $dom $query2xml->getXML(
  13.     "SELECT
  14.         *
  15.      FROM
  16.         artist
  17.      ORDER BY
  18.         artistid",
  19.     array(
  20.         'rootTag' => 'music_library',
  21.         'rowTag' => 'artist',
  22.         'idColumn' => 'artistid',
  23.         'elements' => array(
  24.             'name',
  25.             'albums' => array(
  26.                 'sql' => array(
  27.                     'data' => array(
  28.                         'artistid'
  29.                     ),
  30.                     'query' => 'SELECT * FROM album WHERE artist_id = ?'
  31.                 ),
  32.                 'rowTag' => 'album',
  33.                 'idColumn' => 'albumid',
  34.                 'elements' => array(
  35.                     'title'
  36.                 )
  37.             )
  38.         )
  39.     )
  40. );
  41. print $dom->saveXML();
  42. ?>
This will write the following to debug.log:
Apr 18 20:54:30 XML_Query2XML [info] QUERY: SELECT
        *
     FROM
        artist
     ORDER BY
        artistid
Apr 18 20:54:30 XML_Query2XML [info] DONE
Apr 18 20:54:30 XML_Query2XML [info] QUERY: SELECT * FROM album WHERE artist_id = ?; DATA:1
Apr 18 20:54:30 XML_Query2XML [info] DONE
Apr 18 20:54:30 XML_Query2XML [info] QUERY: SELECT * FROM album WHERE artist_id = ?; DATA:2
Apr 18 20:54:30 XML_Query2XML [info] DONE
Apr 18 20:54:30 XML_Query2XML [info] QUERY: SELECT * FROM album WHERE artist_id = ?; DATA:3
Apr 18 20:54:30 XML_Query2XML [info] DONE
   
XML_Query2XML simply logs the SQL SELECT string the way it was passed to getXML(), including all whitespace characters. If the query in question is a complex join over multiple tables this might be a good thing. On the other hand you might just want to have a single line per log entry. This is best achieved by building a wrapper around PEAR Log:
  1. class MyLogger {
  2.     private $_logger null;
  3.     public function __construct($logger)
  4.     {
  5.         $this->_logger $logger;
  6.     }
  7.     
  8.     public function log($msg)
  9.     {
  10.         $this->_logger->log(preg_replace('/\n\ +/m'' '$msg));
  11.     }
  12. }
Using this wrapper like this:
  1. $query2xml->enableDebugLog(
  2.     new MyLogger(
  3.         Log::factory('file''debug.log''XML_Query2XML')
  4.     )
  5. );
will make the log file from the prvious example look like this:
Apr 18 20:55:51 XML_Query2XML [info] QUERY: SELECT * FROM artist ORDER BY artistid
Apr 18 20:55:51 XML_Query2XML [info] DONE
Apr 18 20:55:51 XML_Query2XML [info] QUERY: SELECT * FROM album WHERE artist_id = ?; DATA:1
Apr 18 20:55:51 XML_Query2XML [info] DONE
Apr 18 20:55:51 XML_Query2XML [info] QUERY: SELECT * FROM album WHERE artist_id = ?; DATA:2
Apr 18 20:55:51 XML_Query2XML [info] DONE
Apr 18 20:55:51 XML_Query2XML [info] QUERY: SELECT * FROM album WHERE artist_id = ?; DATA:3
Apr 18 20:55:51 XML_Query2XML [info] DONE
   
Note how the first query is now placed on a single line.

XML_Query2XML::enableDebugLog()

Please see the API docs at XML_Query2XML::enableDebugLog().

XML_Query2XML::disableDebugLog()

Please see the API docs at XML_Query2XML::disableDebugLog().

Profiling and Performance Tuning

When the amount of data you have to deal with is getting bigger and bigger you will start to ask yourself questions like:

  • Are two smaller joins faster than a single huge one?
  • Should I use $options['sql'] with or without caching?
  • How often does a certain query get executed and how long does it take?
XML_Query2XML's profiling provides help on giving answers to these questions.

Example

XML_Query2XML::startProfiling() should be the first and XML_Query2XML::getProfile() the last method you call on your XML_Query2XML instance.

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. $query2xml XML_Query2XML::factory($db);
  4.  
  5. //start the profiling as soon as possible
  6. $query2xml->startProfiling();
  7.  
  8. //do the real work
  9. $dom $query2xml->getXML(...);
  10. print $dom->saveXML();
  11.  
  12. //save the profile in a separate file
  13. require_once 'File.php';
  14. $fp new File();
  15. $fp->write('/tmp/query2xml_profile.txt'$query2xml->getProfile()FILE_MODE_WRITE);
  16. ?>

XML_Query2XML::startProfiling()

XML_Query2XML::startProfiling() will start the profiling by initializing the private variable XML_Query2XML::$_profile. See XML_Query2XML::getRawProfile() for details on its data format.

XML_Query2XML::stopProfiling()

XML_Query2XML::stopProfiling() will stop the profiling. In most cases you will not need to call this method as XML_Query2XML::getProfile() will do so implicitly.

XML_Query2XML::getProfile()

XML_Query2XML::getProfile() will return the profile as a multiline string. It is a table with the following columns:

  • FROM_DB: number of times this type of query executed in the database
  • FROM_CACHE: number of times the results could be retrieved from cache
  • CACHED: whether caching was performed (true or false); if caching was performed but FROM_CACHE is 0, the value will be "true!" to indicate that no caching is necessary
  • AVG_DURATION: average duration of executing the query and getting it's results
  • DURATION_SUM: total duration for all queries of this type
  • SQL: the query itself
Additionally there will be a summary at the end of the file. It will contain two fields:
  • TOTAL_DURATION: number of seconds the whole operation took (including outputting everything). Whether you output the generated XML data will only affect TOTAL_DURATION but not DB_DURATION.
  • DB_DURATION: number of seconds spent executing SQL queries and retrieving their results.

XML_Query2XML::getRawProfile()

XML_Query2XML::getRawProfile() will return the raw profile data as a multi dimensional associative array. It has the following format:

  1. $this->_profile array(
  2.     'queries'    => array(),
  3.     'start'      => microtime(1),
  4.     'stop'       => 0,
  5.     'duration'   => 0,
  6.     'dbStop'     => 0,
  7.     'dbDuration' => 0
  8. );
The element 'queries' is itself an associative array that uses $sql as the array key;:
  1. $this->_profile['queries'][$sqlarray(
  2.     'fromDB' => 0,
  3.     'fromCache' => 0,
  4.     'cached' => false,
  5.     'runTimes' => array()
  6. );
The element 'runTimes' is an indexed array that stores multiple arrays that have the following format:
  1. array('start' => microtime(true)'stop' => 0);

XML_Query2XML::clearProfile()

Please see the API docs at XML_Query2XML::clearProfile().

The LDAP Driver

Since v1.6.0RC1 XML_Query2XML comes with a driver for PEAR Net_LDAP. The driver for PEAR Net_LDAP2 is available since v1.7.0RC1. This allows you to use LDAP instead of an RDBMS as your primary data source. To use the PEAR Net_LDAP driver you can pass an instance of Net_LDAP to XML_Query2XML::factory(). The only thing that changes is the format of $sql and $options['sql'].

LDAP Query Specification

All LDAP queries have to be specified as an array. LDAP query specifications can be used for $sql and $options['sql']. They look as follows:

  1. array(
  2.     'data' => array(...),  // optional; only relevant if placeholders are used in 'base' or 'filter'
  3.     'base' => 'ou=peopole,dc=example,dc=com',
  4.     'filter' => '(objectclass=inetOrgPerson)',
  5.     'options' => array(
  6.         'attributes' => array(
  7.             ...
  8.         )...
  9.     )
  10. )
These are the arguments as they will be passed to Net_LDAP::search(). Please see the Net_LDAP manual for details.

Note that $ldapQuery['options']['attributes'] will be used by the LDAP driver if it is present. It allows the driver to set the specified attributes to null if they are not present in the returned records. It is therefore highly recommended to always set the option 'attributes'. See Handling Optional Attributes - $options['query']['options']['attributes'] for details.

  1. array(
  2.     'base' => 'ou=peopole,dc=example,dc=com',
  3.     'filter' => '(objectclass=inetOrgPerson)',
  4.     'options' => array(
  5.         'attributes' => array(
  6.             'cn',
  7.             'mobile'
  8.         )
  9.     )
  10. )

Now let's look at an example. We want all entries from ou=people,dc=example,dc=com that have an objectclass of inetOrgPerson. For each of these entries we want to output the attributes cn and mobile.

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'Net/LDAP.php';
  4.  
  5. /*
  6. * just pass an instance of Net_LDAP to the factory method
  7. * as you would do it with an MDB2 or PDO instance.
  8. */
  9. $query2xml XML_Query2XML::factory(Net_LDAP::connect());
  10.  
  11. $dom $query2xml->getXML(
  12.     array(  //this is different than what you're used to from the other drivers
  13.         'base' => 'ou=people,dc=example,dc=com',
  14.         'filter' => '(objectclass=inetOrgPerson)',
  15.         'options' => array(
  16.             'attributes' => array//to tell the LDAP driver which columns to watch out for
  17.                 'cn',
  18.                 'mobile',
  19.             )
  20.         )
  21.     ),
  22.     array(
  23.         'rootTag' => 'employees',
  24.         'idColumn' => 'cn',
  25.         'rowTag' => 'employee',
  26.         'elements' => array(
  27.             'cn',
  28.             'mobile'
  29.         )
  30.     )
  31. );
  32. header('Content-Type: application/xml');
  33.  
  34. $dom->formatOutput true;
  35. print $dom->saveXML();
  36. ?>
This would produce the following XML data:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <employees>
  3.   <employee>
  4.     <cn>John Doe</cn>
  5.     <mobile>666-777-888</mobile>
  6.   </employee>
  7.   <employee>
  8.     <cn>Jane Doe</cn>
  9.     <mobile>555-777-888</mobile>
  10.   </employee>
  11. </employees>

An LDAP Query Specification that makes use of placeholders and the option 'data' looks like this:

  1. array(
  2.     'data' => array(':John Doe'),
  3.     'base' => 'ou=peopole,dc=example,dc=com',
  4.     'filter' => '(&(objectclass=inetOrgPerson)(cn=?))',
  5.     'options' => array(
  6.         'attributes' => array(
  7.             'cn',
  8.             'mobile'
  9.         ),
  10.         'query2xml_placeholder' => '?'
  11.     )
  12. )
An LDAP query specification provides a prepare&execute-like funktionality: a placeholder (by default a question mark) can be used in ['base'] and ['filter']. That placeholder will be replaced with the values from ['data']. Please note that a placeholder will only be treated as such if there is a corresponsing element in the 'data' array. That means that if the 'data' array has only one element and 'base' and 'filter' contain more than one quesion mark, all but the first quesion mark will not be replaced.

Also note that there is another option the LDAP driver will understand: 'query2xml_placeholder'. It allows you to define the placeholder used in 'base' and 'filter'. The default is a question mark ('?'). The placeholder you define can also consist of multiple chracters.

Let's have a look at a full example: we want all entries of the class inetOrgPerson that have a cn attribute that contains the string submitted vi $_GET['cn']. Please note that the LDAP driver internally uses Net_LDAP_Util::escape_filter_value() for all elements of the 'data' array to prevent injection attacks.

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'Net/LDAP.php';
  4.  
  5. $query2xml XML_Query2XML::factory(Net_LDAP::connect());
  6.  
  7. $dom $query2xml->getXML(
  8.     array(
  9.         'data' => array(':' $_GET['cn']),
  10.         'base' => 'ou=people,dc=example,dc=com',
  11.         'filter' => '(&(objectclass=inetOrgPerson)(cn=*?*))'//the question mark will be replace
  12.                                                               //with the contents of $_GET['cn']
  13.         'options' => array(
  14.             'attributes' => array//to tell the LDAP driver which columns to watch out for
  15.                 'cn',
  16.                 'mobile',
  17.             )
  18.         )
  19.     ),
  20.     array(
  21.         'rootTag' => 'employees',
  22.         'idColumn' => 'cn',
  23.         'rowTag' => 'employee',
  24.         'elements' => array(
  25.             'cn',
  26.             'mobile'
  27.         )
  28.     )
  29. );
  30. header('Content-Type: application/xml');
  31.  
  32. $dom->formatOutput true;
  33. print $dom->saveXML();
  34. ?>
if $_GET['cn'] was set to 'John' the following XML data would be produced:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <employees>
  3.   <employee>
  4.     <cn>John Doe</cn>
  5.     <mobile>666-777-888</mobile>
  6.   </employee>
  7. </employees>

Handling Optional Attributes - $options['query']['options']['attributes']

In an LDAP directory an entry does not have to use all attributes that are provided by the entry's objectClass. In the following example the attributes 'mail' and 'mobile' (among others) are missing from the first entry.

dn: cn=Jane Doe,ou=people,dc=example,dc=com
cn: Jane Doe
objectClass: inetOrgPerson

dn: cn=John Doe,ou=people,dc=example,dc=com
cn: John Doe
mail: john.doe@example.com
mobile: 555-666-777
objectClass: inetOrgPerson
    
This would potentially lead to problems as it is contrary to what is possible in a RDBMS, where every record has to have a value for each column (even if it's NULL). XML_Query2XML was primarily built with an RDBMS in mind and therefore expects all data returned by a driver to be records represented as an array of associative arrays (where each associative array uses the same keys).

To solve this problem the columns corresponding to missing attributes have to be set to null. But to tell the LDAP driver which columns to check for, you have to use the $options['query']['options']['attributes']. (Note: $options['query'] is an alias for $options['sql'])

$options['query']['options']['attributes'] will be used by Net_LDAP to limit the attributes returned for each record (see the Net_LDAP manual). In addition to this, XML_Query2XML's LDAP driver will look at this option to determine which columns to set to null if they do not exist in the returned records.

Let's look at what happens if you do not specify the 'attributes' option:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'Net/LDAP.php';
  4.  
  5. /*
  6. * just pass an instance of Net_LDAP to the factory method
  7. * as you would do it with an MDB2 or PDO instance.
  8. */
  9. $query2xml XML_Query2XML::factory(Net_LDAP::connect());
  10.  
  11. $dom $query2xml->getXML(
  12.     array(
  13.         'base' => 'ou=people,dc=example,dc=com',
  14.         'filter' => '(objectclass=inetOrgPerson)'
  15.         //we completely omit 'options' here
  16.     ),
  17.     array(
  18.         'rootTag' => 'employees',
  19.         'idColumn' => 'cn',
  20.         'rowTag' => 'employee',
  21.         'elements' => array(
  22.             'cn',
  23.             'pager'
  24.         )
  25.     )
  26. );
  27. header('Content-Type: application/xml');
  28.  
  29. $dom->formatOutput true;
  30. print $dom->saveXML();
  31. ?>
You would get something like this:
Fatal error: Uncaught XML_Query2XML_ConfigException: [elements][pager]: The column "pager" was not found in the result set.
    
This is because Jane Doe does not have a pager. As the LDAP driver was not told that the 'pager' attribute might be missing from some entries, it returned Jane Doe's record without the column 'pager'. An LDAP query specification should therefore always use the 'attributes' option!

Handling Multi-Value Attributes

In an LDAP directory, attributes are mutli-valued, that is they can hold multiple values. This is nice because it allows you to easily store, say not just one email address, but as many as you like. This is contrary to what you can usually do in an RDBMS. As XML_Query2XML was built primarily with an RDBMS in mind, it expects simple records, each represented by a one-dimensional array to be returned by the LDAP driver.

The LDAP driver therefore creates multiple records for each entry that has multi-value attributes. An entry like

## LDIF entry for "John Doe"
dn: cn=John Doe,ou=people,dc=example,dc=com
cn: John Doe
sn: Doe
mail: john@example.com
mail: johndoe@example.com
mail: john.doe@example.com
telephoneNumber: 555-111-222
telephoneNumber: 555-222-333
mobile: 666-777-888
    
therefore has to be converted into multiple one-dimensional associative arrays (i.e. records):
    cn        mail                  telephoneNumber  mobile
    -------------------------------------------------------
    John Doe  john@example.com      555-111-222      666-777-888
    John Doe  johndoe@example.com   555-222-333      666-777-888
    John Doe  john.doe@example.com  555-111-222      666-777-888
    
Note that no cartasian product of the mail-values and the mobile-values is produced (that would result in six instead of three records in the above example). The number of records returned is equal to the number values assigned to the attribute that has the most values (here it's the mail attribute that has 3 values). To make sure that every record has valid values for all attributes/columns, we continiusly loop over the available value until the attribute with the most values is done. In the above example the attribute telephoneNumber had only two values while the mail attribute had three. The record for the third mail attribute value therefore contains the first value of the telephoneNumber attribute.

So what does that mean for $options (the second argument passed to getXML())? It means that not much changes compared to how you would define $options when using a database-related driver. As shown above, using Simple Element Specifications will work, but will only produce the first value for each attribute:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'Net/LDAP.php';
  4.  
  5. $query2xml XML_Query2XML::factory(Net_LDAP::connect());
  6.  
  7. $dom $query2xml->getXML(
  8.     array(
  9.         'base' => 'ou=people,dc=example,dc=com',
  10.         'filter' => '(objectclass=inetOrgPerson)',
  11.         'options' => array(
  12.             'attributes' => array//to tell the LDAP driver which columns to watch out for
  13.                 'cn',
  14.                 'mail',
  15.             )
  16.         )
  17.     ),
  18.     array(
  19.         'rootTag' => 'employees',
  20.         'idColumn' => 'cn',
  21.         'rowTag' => 'employee',
  22.         'elements' => array(
  23.             'cn',     //simple element specification
  24.             'mail'    //simple element specification
  25.         )
  26.     )
  27. );
  28. header('Content-Type: application/xml');
  29.  
  30. $dom->formatOutput true;
  31. print $dom->saveXML();
  32. ?>
notice how the resulting XML only shows the first value of the mail attribute:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <employees>
  3.   <employee>
  4.     <cn>John Doe</cn>
  5.     <mail>john@example.com</mail>
  6.   </employee>
  7.   <employee>
  8.     <cn>Jane Doe</cn>
  9.     <mail>jane@example.com</mail>
  10.   </employee>
  11. </employees>
To get a "mail" XML element for each value of the "mail" LDAP attribute, we have to use Complex Element Specifications and set $options['idColumn']:
  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'Net/LDAP.php';
  4.  
  5. $query2xml XML_Query2XML::factory(Net_LDAP::connect());
  6.  
  7. $dom $query2xml->getXML(
  8.     array(
  9.         'base' => 'ou=people,dc=example,dc=com',
  10.         'filter' => '(objectclass=inetOrgPerson)',
  11.         'options' => array(
  12.             'attributes' => array//to tell the LDAP driver which columns to watch out for
  13.                 'cn',
  14.                 'mail',
  15.             )
  16.         )
  17.     ),
  18.     array(
  19.         'rootTag' => 'employees',
  20.         'idColumn' => 'cn',
  21.         'rowTag' => 'employee',
  22.         'elements' => array(
  23.             'cn',     //simple element specification
  24.             'mail' => array(
  25.                 'idColumn' => 'mail',
  26.                 'value' => 'mail'
  27.             )
  28.         )
  29.     )
  30. );
  31. header('Content-Type: application/xml');
  32.  
  33. $dom->formatOutput true;
  34. print $dom->saveXML();
  35. ?>
This produces the following XML data:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <employees>
  3.   <employee>
  4.     <cn>John Doe</cn>
  5.     <mail>john@example.com</mail>
  6.     <mail>johndoe@example.com</mail>
  7.     <mail>john.doe@example.com</mail>
  8.   </employee>
  9.   <employee>
  10.     <cn>Jane Doe</cn>
  11.     <mail>jane@example.com</mail>
  12.     <mail>jane.doe@example.com</mail>
  13.   </employee>
  14. </employees>
This is because we set $options['idColumn'] within the complex element specification to "mail". Every record with a unique value for the column "mail" is therefore processed.

As all LDAP attributes can have multiple values it is therefore highly recommended to always use a complex element specification with $options['idColumn'] as described above. The only exception is when you just want a single value (and don't care which one).

Using Multiple Drivers

There might be a situation where you want to genearte your XML data from multiple RDBMSes or an RDBMS and an LDAP server. By using the option 'driver' within a Complex Query Specification or a LDAP Query Specification you can specify any driver you want. To create an instance of one of the drivers that come with XML_Query2XML use XML_Query2XML_Driver::factory(). If you want to write your own driver, please see Writing Your Own Driver. Let's start with an example that pulls data from a MySQL and a ProstgreSQL database:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4.  
  5. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  6.  
  7. $pgsqlDriver XML_Query2XML_Driver::factory(MDB2::factory('pgsql://postgres:test@localhost/query2xml_tests'));
  8. $dom $query2xml->getXML(
  9.     'SELECT * FROM artist',
  10.     array(
  11.         'rootTag' => 'artists',
  12.         'idColumn' => 'artistid',
  13.         'rowTag' => 'artist',
  14.         'elements' => array(
  15.             'name',
  16.             'genre',
  17.             'album' => array(
  18.                 'sql' => array(
  19.                     'driver' => $pgsqlDriver,
  20.                     'data' => array(
  21.                         'artistid'
  22.                     ),
  23.                     'query' => 'SELECT * FROM album WHERE artist_id = ?'
  24.                 ),
  25.                 'idColumn' => 'albumid',
  26.                 'elements' => array(
  27.                     'title',
  28.                     'published_year'
  29.                 )
  30.             )
  31.         )
  32.     )
  33. );
  34. header('Content-Type: application/xml');
  35.  
  36. $dom->formatOutput true;
  37. print $dom->saveXML();
  38. ?>
The resulting XML being:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <artists>
  3.   <artist>
  4.     <name>Curtis Mayfield</name>
  5.     <genre>Soul</genre>
  6.     <album>
  7.       <title>New World Order</title>
  8.       <published_year>1990</published_year>
  9.     </album>
  10.     <album>
  11.       <title>Curtis</title>
  12.       <published_year>1970</published_year>
  13.     </album>
  14.   </artist>
  15.   <artist>
  16.     <name>Isaac Hayes</name>
  17.     <genre>Soul</genre>
  18.     <album>
  19.       <title>Shaft</title>
  20.       <published_year>1972</published_year>
  21.     </album>
  22.   </artist>
  23.   <artist>
  24.     <name>Ray Charles</name>
  25.     <genre>Country and Soul</genre>
  26.   </artist>
  27. </artists>

Now let's combine an RDBMS with the data from an LDAP server:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'Net/LDAP.php';
  4. require_once 'MDB2.php';
  5. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  6.  
  7. $dom $query2xml->getXML(
  8.   "SELECT * FROM employee",
  9.   array(
  10.     'rootTag' => 'employees',
  11.     'idColumn' => 'employeeid',
  12.     'rowTag' => 'employee',
  13.     'elements' => array(
  14.         'name' => 'employeename',
  15.         'contact_details' => array(
  16.             'query' => array(
  17.                 'driver' => XML_Query2XML_Driver::factory(Net_LDAP::connect()),
  18.                 'data' => array(
  19.                     'employeename'
  20.                 ),
  21.                 'base' => 'ou=people,dc=example,dc=com',
  22.                 'filter' => '(&(objectclass=inetOrgPerson)(cn=?))',
  23.                 'options' => array(
  24.                     'attributes' => array(
  25.                         'cn',
  26.                         'telephoneNumber',
  27.                         'mobile',
  28.                         'mail',
  29.                         'labeledURI'
  30.                     )
  31.                 )
  32.             ),
  33.             'idColumn' => 'cn',
  34.             'elements' => array(
  35.                 'telephoneNumber' => array(
  36.                     'idColumn' => 'telephoneNumber',
  37.                     'value' => 'telephoneNumber'
  38.                 ),
  39.                 'mobile' => array(
  40.                     'idColumn' => 'mobile',
  41.                     'value' => 'mobile'
  42.                 ),
  43.                 'mail' => array(
  44.                     'idColumn' => 'mail',
  45.                     'value' => 'mail'
  46.                 ),
  47.                 'labeledURI' => array(
  48.                     'idColumn' => 'labeledURI',
  49.                     'value' => 'labeledURI'
  50.                 )
  51.             )
  52.         )
  53.     ),
  54.   )
  55. );
  56. $dom->formatOutput true;
  57. print $dom->saveXML();
  58. ?>
As we only have corresponsing entries in the LDAP directory for two of our employees in the mysql database the resulting XML looks like this:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <employees>
  3.   <employee>
  4.     <name>Michael Jones</name>
  5.   </employee>
  6.   <employee>
  7.     <name>Susi Weintraub</name>
  8.     <contact_details>
  9.       <telephoneNumber>555-111-222</telephoneNumber>
  10.       <mobile>555-666-777</mobile>
  11.       <mobile>555-777-888</mobile>
  12.       <mail>susi@example.com</mail>
  13.       <labeledURI>http://susi.example.com</labeledURI>
  14.       <labeledURI>http://susiweintraub.example.com</labeledURI>
  15.       <labeledURI>http://susi.weintraub.example.com</labeledURI>
  16.     </contact_details>
  17.   </employee>
  18.   <employee>
  19.     <name>Steve Hack</name>
  20.   </employee>
  21.   <employee>
  22.     <name>Joan Kerr</name>
  23.   </employee>
  24.   <employee>
  25.     <name>Marcus Roth</name>
  26.   </employee>
  27.   <employee>
  28.     <name>Jack Mack</name>
  29.   </employee>
  30.   <employee>
  31.     <name>Rita Doktor</name>
  32.   </employee>
  33.   <employee>
  34.     <name>David Til</name>
  35.   </employee>
  36.   <employee>
  37.     <name>Pia Eist</name>
  38.   </employee>
  39.   <employee>
  40.     <name>Hanna Poll</name>
  41.   </employee>
  42.   <employee>
  43.     <name>Jim Wells</name>
  44.     <contact_details>
  45.       <telephoneNumber>555-444-888</telephoneNumber>
  46.       <mobile>555-666-777</mobile>
  47.       <mail>jim@example.com</mail>
  48.       <mail>jim.wells@example.com</mail>
  49.       <mail>jimwells@example.com</mail>
  50.       <mail>jwells@example.com</mail>
  51.       <labeledURI>http://jimwells.example.com</labeledURI>
  52.       <labeledURI>http://jim.wells.example.com</labeledURI>
  53.     </contact_details>
  54.   </employee>
  55.   <employee>
  56.     <name>Sandra Wilson</name>
  57.   </employee>
  58. </employees>

Implementing 1:N and N:N relationships

Often times you will have a one-to-many (1:N) or many-to-many (N:N) relationship between two tables. For example, the ER diagram of our sample applications shows an 1:N relationship between the tables artist and album (one artist can perform many albums). For the sake of simplicity we will be using this 1:N relationship in this section (note that the intersection table employee_department implements an N:N relationship between employee and department).

When dealing with 1:N or N:N relationships there are two basic questions that determine how you will use XML_Query2XML to implement your solution:

One or Many Queries?

In almost every situation you will be better of using one big query than running multiple smaller queries. Anyway, XML_Query2XML gives you the option of

Using an SQL JOIN

The best way to query two tables is to JOIN them together using an SQL JOIN. If you are new to SQL check out first what wikipedia has to say about Joins. I could also recommend Learning SQL by Alan Beaulieu.

Use XML_Query2XML::getXML()'s first argument $sql to pass the SQL JOIN to XML_Query2XML.

A very important thing to keep in mind is the correct specification of $options['idColumn']. On the root level it is set to 'artistid' (the parent entity's primary key) while on the level of $options['elements']['albums'] it is set to 'albumid' (the child entity's primary key). Here goes the code:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.     'SELECT
  7.         *
  8.      FROM
  9.         artist, album
  10.      WHERE
  11.         album.artist_id = artist.artistid',
  12.     array(
  13.         'rootTag' => 'music_library',
  14.         'rowTag' => 'artist',
  15.         'idColumn' => 'artistid',
  16.         'elements' => array(
  17.             'artistid',
  18.             'name',
  19.             'albums' => array(
  20.                 'rootTag' => 'albums',
  21.                 'rowTag' => 'album',
  22.                 'idColumn' => 'albumid',
  23.                 'elements' => array(
  24.                     'albumid',
  25.                     'title'
  26.                 )
  27.             )
  28.         )
  29.     )
  30. );
  31.  
  32. header('Content-Type: application/xml');
  33.  
  34. $dom->formatOutput true;
  35. print $dom->saveXML();
  36. ?>
And the resulting XML data looks as follows:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <music_library>
  3.   <artist>
  4.     <artistid>1</artistid>
  5.     <name>Curtis Mayfield</name>
  6.     <albums>
  7.       <album>
  8.         <albumid>1</albumid>
  9.         <title>New World Order</title>
  10.       </album>
  11.       <album>
  12.         <albumid>2</albumid>
  13.         <title>Curtis</title>
  14.       </album>
  15.     </albums>
  16.   </artist>
  17.   <artist>
  18.     <artistid>2</artistid>
  19.     <name>Isaac Hayes</name>
  20.     <albums>
  21.       <album>
  22.         <albumid>3</albumid>
  23.         <title>Shaft</title>
  24.       </album>
  25.     </albums>
  26.   </artist>
  27. </music_library>
Note: as we used a traditional join (and therefore implecitely an INNER JOIN) only those artists are included that also have albums in our database. To also get those artists that don't have any albums we would have to use a LEFT OUTER JOIN.


Using $options['sql']

If for whatever reason you cannot join the two tables, you can use $options['sql'] to specify a separate query for the second table. This will however result in the second query being executed as many times as the there are records in the first table. It is therefore highly recommended to use a JOIN whenever possible.

To produce a similar XML as with the JOIN above we have to use a Complex Query Specification with artist.artistid for $options['elements']['albums']['sql']['data'] and 'SELECT * FROM album WHERE artist_id = ?' for $options['elements']['albums']['sql']['query']:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.     'SELECT * FROM artist',
  7.     array(
  8.         'rootTag' => 'music_library',
  9.         'rowTag' => 'artist',
  10.         'idColumn' => 'artistid',
  11.         'elements' => array(
  12.             'artistid',
  13.             'name',
  14.             'albums' => array(
  15.                 'rootTag' => 'albums',
  16.                 'rowTag' => 'album',
  17.                 'idColumn' => 'albumid',
  18.                 'sql' => array(
  19.                     'data' => array(
  20.                         'artistid'
  21.                     ),
  22.                     'query' => 'SELECT * FROM album WHERE artist_id = ?'
  23.                 ),
  24.                 'elements' => array(
  25.                     'albumid',
  26.                     'title'
  27.                 )
  28.             )
  29.         )
  30.     )
  31. );
  32.  
  33. header('Content-Type: application/xml');
  34.  
  35. $dom->formatOutput true;
  36. print $dom->saveXML();
  37. ?>
Note that $options['elements']['albums']['idColumn'] is again set to 'albumid'.

The difference to the above XML is that it also includes artists that have no albums in our database. This is because using two separate queries is like using an OUTER JOIN while in the above example we were using an INNER JOIN.

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <music_library>
  3.   <artist>
  4.     <artistid>1</artistid>
  5.     <name>Curtis Mayfield</name>
  6.     <albums>
  7.       <album>
  8.         <albumid>1</albumid>
  9.         <title>New World Order</title>
  10.       </album>
  11.       <album>
  12.         <albumid>2</albumid>
  13.         <title>Curtis</title>
  14.       </album>
  15.     </albums>
  16.   </artist>
  17.   <artist>
  18.     <artistid>2</artistid>
  19.     <name>Isaac Hayes</name>
  20.     <albums>
  21.       <album>
  22.         <albumid>3</albumid>
  23.         <title>Shaft</title>
  24.       </album>
  25.     </albums>
  26.   </artist>
  27.   <artist>
  28.     <artistid>3</artistid>
  29.     <name>Ray Charles</name>
  30.     <albums/>
  31.   </artist>
  32. </music_library>


One or Many Elements Per Related Entity?

Depending on whether you want one or multiple XML element per related entity, you have two options here:

Multiple Elements Per Entity - $options['elements']

In most cases you will need to create multiple XML elements for each related entity. In our example we might want the albumid and the title for each album. This means that we have to assign

  1. array(
  2.     'albumid',
  3.     'title'
  4. );
to $options['elements']['albums']['elements'].

Please see the One or Many Queries? section above for two example - one using a JOIN and the second using two queries.


One Element Per Entity - $options['value']

In our example, if you want just a single XML element for each album like below, we can use $options['value'].

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <music_library>
  3.   <artist>
  4.     <name>Curtis Mayfield</name>
  5.     <album>New World Order</album>
  6.     <album>Curtis</album>
  7.   </artist>
  8.   <artist>
  9.     <name>Isaac Hayes</name>
  10.     <album>Shaft</album>
  11.   </artist>
  12. </music_library>
The code to generate the above XML would look like this:
  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.     'SELECT
  7.         *
  8.      FROM
  9.         artist, album
  10.      WHERE
  11.         album.artist_id = artist.artistid',
  12.     array(
  13.         'rootTag' => 'music_library',
  14.         'rowTag' => 'artist',
  15.         'idColumn' => 'artistid',
  16.         'elements' => array(
  17.             'name',
  18.             'album' => array(
  19.                 'idColumn' => 'albumid',
  20.                 'value' => 'title'
  21.             )
  22.         )
  23.     )
  24. );
  25.  
  26. header('Content-Type: application/xml');
  27.  
  28. $dom->formatOutput true;
  29. print $dom->saveXML();
  30. ?>
The critical part here is $options['elements']['albums']['idColumn']. It is set to 'albumid'. This means that an XML element will be created for $options['elements']['albums'] for each unique albumid.

The above example uses a JOIN which is most efficient. But as shown above under Using $options['sql'] there might be situations where you want to use multiple SQL queries instead of a single JOIN:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.     'SELECT * FROM artist',
  7.     array(
  8.         'rootTag' => 'music_library',
  9.         'rowTag' => 'artist',
  10.         'idColumn' => 'artistid',
  11.         'elements' => array(
  12.             'name',
  13.             'album' => array(
  14.                 'sql' => array(
  15.                     'data' => array(
  16.                         'artistid'
  17.                     ),
  18.                     'query' => 'SELECT * FROM album WHERE artist_id = ?'
  19.                 ),
  20.                 'idColumn' => 'albumid',
  21.                 'value' => 'title'
  22.             )
  23.         )
  24.     )
  25. );
  26.  
  27. header('Content-Type: application/xml');
  28.  
  29. $dom->formatOutput true;
  30. print $dom->saveXML();
  31. ?>
The resulting XML is similar to the one generated above. There is just one difference: artists that have no albums are included as well:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <music_library>
  3.   <artist>
  4.     <name>Curtis Mayfield</name>
  5.     <album>New World Order</album>
  6.     <album>Curtis</album>
  7.   </artist>
  8.   <artist>
  9.     <name>Isaac Hayes</name>
  10.     <album>Shaft</album>
  11.   </artist>
  12.   <artist>
  13.     <name>Ray Charles</name>
  14.   </artist>
  15. </music_library>


A last word on JOINs

As a general rule, it is wise to use as few SQL queries as possible. This means that you have to JOIN you tables. It is essential to understand the differences between INNER and OUTER joins and how they affect your result set.

Writing Your Own Driver

If you want to work with a primary data source other than PDO, MDB2, DB, ADOdb or Net_LDAP, you might want to write your own driver.

Start out by extending the abstract class XML_Query2XML_Driver and implementing its abstract method getAllRecords():

  1. /**Contains the abstract class XML_Query2XML_Driver and the
  2. * exception class XML_Query2XML_DBException.
  3. */
  4. require_once 'XML/Query2XML.php';
  5.  
  6. class MyFirebirdDriver extends XML_Query2XML_Driver
  7. {
  8.     /*This method is defined as an abstract method within
  9.     * XML_Query2XML_Driver and therefore has to be implemented.
  10.     */
  11.     public function getAllRecords($sql$configPath)
  12.     {
  13.     }
  14. }
  15. ?>
For the sake of example, we'll be building a Firebird driver using PHP's native interbase API.

Now let's add a constructor method that accepts the arguments needed to set up an Interbase/Firebird connection:

  1. <?php
  2. public function __construct($database$username$password)
  3. {
  4.     $this->_dbh @ibase_pconnect($database$username$password);
  5.     if ($this->_dbh === false{
  6.         throw new XML_Query2XML_DBException(
  7.             'Could not connect to database: ' ibase_errmsg()
  8.             . '(error code: ' ibase_errcode(')'
  9.         );
  10.     }
  11. }
  12. ?>

Before we code the body of our getAllRecords() method, we have to decide whether to overwrite the method XML_Query2XML_Driver::preprocessQuery(). This method does two things:

  • pre-process $options['sql']
  • return the query statement as a string that will be used by XML_Query2XML for logging, profiling and caching
The importance of preprocessQuery() lies in the fact that it determines the format of $options['sql']. As implemented in XML_Query2XML_Driver $options['sql'] can be either a string or an array containing the element 'query'. If it is a string it will be transformed to an array with the element 'query' holding the string. This means that getAllRecords() only needs to deal with a first argument that is a string and has a 'query' element. In this example we will not overwrite preprocessQuery().

One aspect that is not controlled by preprocessQuery() is $options['sql']['data']. If it is set, XML_Query2XML requires it to be an array of strings or instances of classes that implement XML_Query2XML_Callback. The evaluations of the specifications in $options['sql']['data'] are entirely handled by XML_Query2XML.

All that is left now is writing the body of the getAllRecords() method. Regarding its first argument, XML_Query2XML_Driver::preprocessQuery() enforces the following rules

  • It will be an associative array.
  • The array will have a key named 'query'.

A first version of getAllRecords() might therefore look like this:

  1. <?php
  2. public function getAllRecords($sql$configPath)
  3. {
  4.     if (!isset($sql['data'])) {
  5.         //do not presume that $sql['data'] is present
  6.         $sql['data'array();
  7.     }
  8.     
  9.     //prepare
  10.     $statement @ibase_prepare($this->_dbh$sql['query']);
  11.     
  12.     //execute
  13.     $args $sql['data'];
  14.     array_unshift($args$statement);
  15.     $result call_user_func_array('ibase_execute'$args);
  16.     
  17.     //fetch all records
  18.     while ($record ibase_fetch_assoc($resultIBASE_TEXT)) {
  19.         $records[$record;
  20.     }
  21.     ibase_free_result($result);
  22.     
  23.     return $records;
  24. }
  25. ?>
The above code is already functional but is still missings some important error checking. Below is the final version of our MyFirebirdDriver class, that includes a more robust version of getAllRecords(). Note how getAllRecords()'s second argument, $configPath is used for the exception messages.
  1. <?php
  2. /**Contains the abstract class XML_Query2XML_Driver and the
  3. * exception class XML_Query2XML_DBException.
  4. */
  5. require 'XML/Query2XML.php';
  6. class MyFirebirdDriver extends XML_Query2XML_Driver
  7. {
  8.     private $_dhb null;
  9.     
  10.     public function __construct($database$username$password)
  11.     {
  12.         $this->_dbh @ibase_pconnect($database$username$password);
  13.         if ($this->_dbh === false{
  14.             throw new XML_Query2XML_DBException(
  15.                 'Could not connect to database: ' ibase_errmsg()
  16.                 . '(error code: ' ibase_errcode(')'
  17.             );
  18.         }}
  19.     
  20.     /**Returns are records retrieved by running an SQL query.
  21.     * @param mixed $sql A query string or an array with the elements
  22.     *                   'query' (and optionally 'data').
  23.     * @param string $configPath Where in $options the query was defined.
  24.     * @return array All records as a two-dimensional array.
  25.     */
  26.     public function &getAllRecords($sql$configPath)
  27.     {
  28.         if (!isset($sql['data'])) {
  29.             //do not presume that $sql['data'] is present
  30.             $sql['data'array();
  31.         }
  32.         
  33.         /*
  34.         * prepare
  35.         */
  36.         $statement @ibase_prepare($this->_dbh$sql['query']);
  37.         if ($statement === false{
  38.             throw new XML_Query2XML_DBException(
  39.                 /*
  40.                 * Note how we use $configPath here. This will make
  41.                 * the exception message tell you where in $options
  42.                 * the error needs to be fixed.
  43.                 */
  44.                 $configPath ': Could not prepare query "' $sql['query''": '
  45.                 . ibase_errmsg('(error code: ' ibase_errcode(')'
  46.             );
  47.         }
  48.         
  49.         /*
  50.         * execute
  51.         */
  52.         $args $sql['data'];
  53.         array_unshift($args$statement);
  54.         $result call_user_func_array(
  55.             'ibase_execute',
  56.             $args
  57.         );
  58.         if ($result === false{
  59.             throw new XML_Query2XML_DBException(
  60.                 /*
  61.                 * Note again how we use $configPath here.
  62.                 */
  63.                 $configPath ': Could not execute query: "' $sql['query''": '
  64.                 . ibase_errmsg('(error code: ' ibase_errcode(')'
  65.             );
  66.         elseif ($result === true{
  67.             //empty result set
  68.             $records array();
  69.         else {
  70.             //fetch all records
  71.             while ($record ibase_fetch_assoc($resultIBASE_TEXT)) {
  72.                 $records[$record;
  73.             }
  74.             ibase_free_result($result);
  75.         }
  76.         
  77.         /*
  78.         * return a two dimensional array: numeric indexes in the first
  79.         * and associative arrays in the second dimension
  80.         */
  81.         return $records;
  82.     }
  83. }
  84.  
  85. //test the driver directly
  86. $driver new MyFirebirdDriver('localhost:/tmp/test.fdb''SYSDBA''test');
  87. $records $driver->getAllRecords(
  88.     array(
  89.         'data' => array(1),
  90.         'query' => 'SELECT * FROM test WHERE id = ?'
  91.     ),
  92.     '[config]'
  93. );
  94. print_r($records);
  95. ?>

Using XML_Query2XML_DBException for a database-related driver makes sense of course. But when your driver has nothing to do with an RDBMS, it is recommended to create your own exception class by extending XML_Query2XML_DriverException:

  1. <?php
  2. /**Exception for MyDriver errors
  3. */
  4. class MyDriverException extends XML_Query2XML_DriverException
  5. {
  6.     /**Constructor
  7.     * @param string $message The error message.
  8.     */
  9.     public function __construct($message)
  10.     {
  11.         parent::__construct($message);
  12.     }
  13. }
  14. ?>
Note: it is in no way required but I would recommend that all exceptions your driver throws extend XML_Query2XML_DriverException. That way you can catch driver related exceptions in a consistent way - no matter what driver is used.

Case Studies

Now let's have a look at some of XML_Query2XML's features in action. We'll start out with simple cases. We'll turn to rather complex ones as we proceed. All cases are included in the source distribution. Each case has its own directory cases/caseXX and will consist of 5 files (Case 01 contains only the first 2):

  • caseXX.php: generates the XML data.
  • caseXX.xml: the generated the XML data saved to a file.
  • caseXX_debug.php: does debugging and profiling and generates caseXX.log and caseXX.profile.
  • caseXX.log: the generated debug log
  • caseXX.profile: the generated profile

The SQL DDL used in all cases can be found in tests/Query2XML_Tests.sql and SQL DDL used in all examples.

Case 01: simple SELECT with getFlatXML

Case 01 will teach you:

Case 01 is as simple as it can get. We use XML_Query2XML::getFlatXML() to generate flat XML data.

case01.php

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getFlatXML(
  6.     "SELECT
  7.         *
  8.      FROM
  9.         artist",
  10.     'music_library',
  11.     'artist');
  12.  
  13. header('Content-Type: application/xml');
  14.  
  15. $dom->formatOutput true;
  16. print $dom->saveXML();
  17. ?>


case01.xml

The result looks like this:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <music_library>
  3.   <artist>
  4.     <artistid>1</artistid>
  5.     <name>Curtis Mayfield</name>
  6.     <birth_year>1920</birth_year>
  7.     <birth_place>Chicago</birth_place>
  8.     <genre>Soul</genre>
  9.   </artist>
  10.   <artist>
  11.     <artistid>2</artistid>
  12.     <name>Isaac Hayes</name>
  13.     <birth_year>1942</birth_year>
  14.     <birth_place>Tennessee</birth_place>
  15.     <genre>Soul</genre>
  16.   </artist>
  17.   <artist>
  18.     <artistid>3</artistid>
  19.     <name>Ray Charles</name>
  20.     <birth_year>1930</birth_year>
  21.     <birth_place>Mississippi</birth_place>
  22.     <genre>Country and Soul</genre>
  23.   </artist>
  24. </music_library>


Case 02: LEFT OUTER JOIN

Case 02 will teach you:

Once you have to deal with LEFT JOINs and similar "complex" SQL queries, you have to use XML_Query2XML::getXML(). The challenge is to get the $options array (getXML's second argument) right:

case02.php

case02.php looks like this:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.     "SELECT
  7.         *
  8.      FROM
  9.         artist
  10.         LEFT JOIN album ON album.artist_id = artist.artistid",
  11.     array(
  12.         'rootTag' => 'music_library',
  13.         'rowTag' => 'artist',
  14.         'idColumn' => 'artistid',
  15.         'elements' => array(
  16.             'artistid',
  17.             'name',
  18.             'birth_year',
  19.             'birth_place',
  20.             'genre',
  21.             'albums' => array(
  22.                 'rootTag' => 'albums',
  23.                 'rowTag' => 'album',
  24.                 'idColumn' => 'albumid',
  25.                 'elements' => array(
  26.                     'albumid',
  27.                     'title',
  28.                     'published_year',
  29.                     'comment'
  30.                 )
  31.             )
  32.         )
  33.     )
  34. );
  35.  
  36. header('Content-Type: application/xml');
  37.  
  38. $dom->formatOutput true;
  39. print $dom->saveXML();
  40. ?>
getXML's first argument is the SQL query as a string. The second is the $options array. Let's go through all options step by step:
  • 'rootTag': we use 'music_library' as the name for the root tag.
  • 'rowTag': all tags on the first level will represent an artis; therefore we name it 'artis'.
  • 'idColumn': we set this to 'artistid' because this is the primary key column of the table artist. At least at this point it should be clear why this option is essential: our LEFT JOIN returns something like this:
    artistid  name                birth_year  albumid album_title     album_published
    -------------------------------------------------------------------------------
    1          Curtis Mayfield    1920        1        New World Order  1990          
    1          Curtis Mayfield    1920        2        Curtis           1970          
    2          Isaac Hayes        1942        3        Shaft            1972          
    3          Ray Charles        1930        NULL     NULL             NULL          
            
    As we want for every artist only a single tag we need to identify each artist by the primary key of the table artist. Note that there is a second record for Curtis Mayfield (related to the album Curtis), but we don't want something like
    1. <artist>
    2.   <name>Curtis Mayfield</name>
    3.   <album>
    4.     <name>New World Order</name>
    5.   </album>
    6. </artist>
    7. <artist>
    8.   <name>Curtis Mayfield</name>
    9.   <album>
    10.     <name>Curits</name>
    11.   </album>
    12. </artist>
    but rather
    1. <artist>
    2.   <name>Curtis Mayfield</name>
    3.   <albums>
    4.     <album>
    5.      <name>New World Order</name>
    6.     </album>
    7.     <albums>
    8.      <name>Curtis</name>
    9.     </albums>
    10.   </albums>
    11. </artist>
    This is achieved by telling XML_Query2XML which entity to focus on (on this level): the artist, as it is identified by the artist table's primary key. Once XML_Query2XML get's to the second Curtis Mayfield record, it can tell by the artistid 1 that an XML element was already created for this artist.
  • 'elements': this is a (not necessarily associative) array of child elements.
    • 'artistid', 'name', 'birth_year', 'birth_place', 'genre': These are simple element specifications. The column name will be used as the element name and the element will only contain the column's value.
    • 'artists': here we use a complex element specification. It is an array that can have all the options that can be present on the root level.
      • 'rootTag': we want all albums to be contained in a singel element named 'albums'.
      • 'rowTag': each album should be contained in an element named 'album'.
      • 'idColumn': here we have to use 'albumid' for the ID column as this is the primary key column for our albums.
      • 'elements': this time, a simple element specification is all we need: 'albumid', 'title', 'published_year', 'comment'


case02.xml

The resulting XML data looks like this:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <music_library>
  3.   <artist>
  4.     <artistid>1</artistid>
  5.     <name>Curtis Mayfield</name>
  6.     <birth_year>1920</birth_year>
  7.     <birth_place>Chicago</birth_place>
  8.     <genre>Soul</genre>
  9.     <albums>
  10.       <album>
  11.         <albumid>1</albumid>
  12.         <title>New World Order</title>
  13.         <published_year>1990</published_year>
  14.         <comment>the best ever!</comment>
  15.       </album>
  16.       <album>
  17.         <albumid>2</albumid>
  18.         <title>Curtis</title>
  19.         <published_year>1970</published_year>
  20.         <comment>that man's got somthin' to say</comment>
  21.       </album>
  22.     </albums>
  23.   </artist>
  24.   <artist>
  25.     <artistid>2</artistid>
  26.     <name>Isaac Hayes</name>
  27.     <birth_year>1942</birth_year>
  28.     <birth_place>Tennessee</birth_place>
  29.     <genre>Soul</genre>
  30.     <albums>
  31.       <album>
  32.         <albumid>3</albumid>
  33.         <title>Shaft</title>
  34.         <published_year>1972</published_year>
  35.         <comment>he's the man</comment>
  36.       </album>
  37.     </albums>
  38.   </artist>
  39.   <artist>
  40.     <artistid>3</artistid>
  41.     <name>Ray Charles</name>
  42.     <birth_year>1930</birth_year>
  43.     <birth_place>Mississippi</birth_place>
  44.     <genre>Country and Soul</genre>
  45.     <albums />
  46.   </artist>
  47. </music_library>


case02_debug.php

XML_Query2XML::getXML() allows us to debug and to profile the operation.

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5.  
  6. require_once 'Log.php';
  7. $debugLogger Log::factory('file''case02.log''Query2XML');
  8. $query2xml->enableDebugLog($debugLogger);
  9.  
  10. $query2xml->startProfiling();
  11.  
  12.  
  13. $dom $query2xml->getXML(
  14.     "SELECT
  15.         *
  16.      FROM
  17.         artist
  18.         LEFT JOIN album ON album.artist_id = artist.artistid",
  19.     array(
  20.         'rootTag' => 'music_library',
  21.         'rowTag' => 'artist',
  22.         'idColumn' => 'artistid',
  23.         'elements' => array(
  24.             'artistid',
  25.             'name',
  26.             'birth_year',
  27.             'birth_place',
  28.             'genre',
  29.             'albums' => array(
  30.                 'rootTag' => 'albums',
  31.                 'rowTag' => 'album',
  32.                 'idColumn' => 'albumid',
  33.                 'elements' => array(
  34.                     'albumid',
  35.                     'title',
  36.                     'published_year',
  37.                     'comment'
  38.                 )
  39.             )
  40.         )
  41.     )
  42. );
  43.  
  44. header('Content-Type: application/xml');
  45.  
  46. $dom->formatOutput true;
  47. print $dom->saveXML();
  48.  
  49. require_once 'File.php';
  50. $fp new File();
  51. $fp->write('case02.profile'$query2xml->getProfile()FILE_MODE_WRITE);
  52. ?>
The lines 5-7 do the debugging, line 9 and 50-52 the profiling. This will create case02.log and case02.profile.


case02.log

The format of a debug log file is documented at Logging and Debugging XML_Query2XML. Our debug log shows that the query runs once.

Feb 11 16:10:36 Query2XML [info] QUERY: SELECT
        *
     FROM
        artist
        LEFT JOIN album ON album.artist_id = artist.artistid
Feb 11 16:10:36 Query2XML [info] DONE
     


case02.profile

Profiling is essential for performance tuning. The format of the output is documented under XML_Query2XML::getProfile(). Our profile looks like this:

FROM_DB FROM_CACHE CACHED AVG_DURATION DURATION_SUM SQL
1       0          false  0.0056409835 0.0056409835 SELECT
        *
     FROM
        artist
        LEFT JOIN album ON album.artist_id = artist.artistid

TOTAL_DURATION: 0.06843900680542
DB_DURATION:    0.015194892883301
     


The value "false" in the CACHED column tells us that no caching was performed. As we can see in the FROM_DB column, the query ran once.

Case 03: Two SELECTs instead of a LEFT OUTER JOIN

Case 03 will teach you:

When your query is getting bigger and bigger (say, 6 or more JOINs) you might want to (or have to, if the maximum number of fields your RDBMS will return has been reached) split up the big join into multiple smaller joins. Here we will just do exactly the same as in Case 02: LEFT OUTER JOIN, but with two separate SELECT queries.

case03.php

case03.php looks like this:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.     "SELECT
  7.         *
  8.      FROM
  9.         artist",
  10.     array(
  11.         'rootTag' => 'music_library',
  12.         'rowTag' => 'artist',
  13.         'idColumn' => 'artistid',
  14.         'elements' => array(
  15.             'artistid',
  16.             'name',
  17.             'birth_year',
  18.             'birth_place',
  19.             'genre',
  20.             'albums' => array(
  21.                 'sql' => array(
  22.                     'data' => array(
  23.                         'artistid'
  24.                     ),
  25.                     'query' => 'SELECT * FROM album WHERE artist_id = ?'
  26.                 ),
  27.                 'rootTag' => 'albums',
  28.                 'rowTag' => 'album',
  29.                 'idColumn' => 'albumid',
  30.                 'elements' => array(
  31.                     'albumid',
  32.                     'title',
  33.                     'published_year',
  34.                     'comment'
  35.                 )
  36.             )
  37.         )
  38.     )
  39. );
  40.  
  41. header('Content-Type: application/xml');
  42.  
  43. $dom->formatOutput true;
  44. print $dom->saveXML();
  45. ?>
We won't go over every option as we did for case02.php. We will only focus on the differences. The first argument to XML_Query2XML::getXML() is a simple SELECT query over one table. What also changed is the complex element specification of 'albums'. It has a new option:
  • 'sql': ['sql']['query'] will be executed for every record retrieved with the first SELECT query. In our case, we want all albums for the current artist record. We use a Complex Query Specification here: ['sql']['data'] contains an array of values that will ultimately be passed to the database abstraction layer's execute() method. As we do not prefix 'artistid' with anything it is interpreted as a column name (of the parent record) - which is just what we want. This completely prevents SQL injection attacks.


case03.xml

The resulting XML data looks exactly like case02.xml:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <music_library>
  3.   <artist>
  4.     <artistid>1</artistid>
  5.     <name>Curtis Mayfield</name>
  6.     <birth_year>1920</birth_year>
  7.     <birth_place>Chicago</birth_place>
  8.     <genre>Soul</genre>
  9.     <albums>
  10.       <album>
  11.         <albumid>1</albumid>
  12.         <title>New World Order</title>
  13.         <published_year>1990</published_year>
  14.         <comment>the best ever!</comment>
  15.       </album>
  16.       <album>
  17.         <albumid>2</albumid>
  18.         <title>Curtis</title>
  19.         <published_year>1970</published_year>
  20.         <comment>that man's got somthin' to say</comment>
  21.       </album>
  22.     </albums>
  23.   </artist>
  24.   <artist>
  25.     <artistid>2</artistid>
  26.     <name>Isaac Hayes</name>
  27.     <birth_year>1942</birth_year>
  28.     <birth_place>Tennessee</birth_place>
  29.     <genre>Soul</genre>
  30.     <albums>
  31.       <album>
  32.         <albumid>3</albumid>
  33.         <title>Shaft</title>
  34.         <published_year>1972</published_year>
  35.         <comment>he's the man</comment>
  36.       </album>
  37.     </albums>
  38.   </artist>
  39.   <artist>
  40.     <artistid>3</artistid>
  41.     <name>Ray Charles</name>
  42.     <birth_year>1930</birth_year>
  43.     <birth_place>Mississippi</birth_place>
  44.     <genre>Country and Soul</genre>
  45.     <albums />
  46.   </artist>
  47. </music_library>


case03_debug.php

case03_debug.php is similar to case02_debug.php:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5.  
  6. require_once 'Log.php';
  7. $debugLogger Log::factory('file''case03.log''Query2XML');
  8. $query2xml->enableDebugLog($debugLogger);
  9.  
  10. $query2xml->startProfiling();
  11.  
  12.  
  13. $dom $query2xml->getXML(
  14.     "SELECT
  15.         *
  16.      FROM
  17.         artist",
  18.     array(
  19.         'rootTag' => 'music_library',
  20.         'rowTag' => 'artist',
  21.         'idColumn' => 'artistid',
  22.         'elements' => array(
  23.             'artistid',
  24.             'name',
  25.             'birth_year',
  26.             'birth_place',
  27.             'genre',
  28.             'albums' => array(
  29.                 'sql' => array(
  30.                     'data' => array(
  31.                         'artistid'
  32.                     ),
  33.                     'query' => 'SELECT * FROM album WHERE artist_id = ?'
  34.                 ),
  35.                 'rootTag' => 'albums',
  36.                 'rowTag' => 'album',
  37.                 'idColumn' => 'albumid',
  38.                 'elements' => array(
  39.                     'albumid',
  40.                     'title',
  41.                     'published_year',
  42.                     'comment'
  43.                 )
  44.             )
  45.         )
  46.     )
  47. );
  48.  
  49. header('Content-Type: application/xml');
  50.  
  51. $dom->formatOutput true;
  52. print $dom->saveXML();
  53.  
  54. require_once 'File.php';
  55. $fp new File();
  56. $fp->write('case03.profile'$query2xml->getProfile()FILE_MODE_WRITE);
  57. ?>
The lines 6-8 do the debugging, line 10 and 54-56 the profiling. This will create case03.log and case03.profile.


case03.log

The format of a debug log file is documented at Logging and Debugging XML_Query2XML. Our debug log now contains 4 queries:

Apr 18 19:00:20 Query2XML [info] QUERY: SELECT
        *
     FROM
        artist
     ORDER BY
        artistid
Apr 18 19:00:20 Query2XML [info] DONE
Apr 18 19:00:20 Query2XML [info] QUERY: SELECT * FROM album WHERE artist_id = ?; DATA:1
Apr 18 19:00:20 Query2XML [info] DONE
Apr 18 19:00:20 Query2XML [info] QUERY: SELECT * FROM album WHERE artist_id = ?; DATA:2
Apr 18 19:00:20 Query2XML [info] DONE
Apr 18 19:00:20 Query2XML [info] QUERY: SELECT * FROM album WHERE artist_id = ?; DATA:3
Apr 18 19:00:20 Query2XML [info] DONE
     

The debug log shows what we expected: the first SELECT over the artist table runs once and the SELECT over the album table runs three times (once for every record found in the artist table). As the log shows no 'CACHING' entries we also know that no cashing was performed ($options['sql_options']['cached'] was not set to true).

case03.profile

Profiling is essential for performance tuning. The format of the output is documented under XML_Query2XML::getProfile(). Our profile looks like this:

FROM_DB FROM_CACHE CACHED AVG_DURATION DURATION_SUM SQL
1       0          false  0.0030851364 0.0030851364 SELECT
        *
     FROM
        artist
3       0          false  0.0035093625 0.0105280876 SELECT * FROM album WHERE artist_id = ?

TOTAL_DURATION: 0.090610980987549
DB_DURATION:    0.024358034133911
     
If you compare our DB_DURATION value to the one in case02.profile you will see that the single LEFT JOIN was faster than the four separate queries.


Case 04: Case 03 with custom tag names, attributes, merge_selective and more

Case 04 will teach you:

  • How to use alternative tag names.
  • How to use callbacks with the '#' prefix.
  • How to define static node and attribute values using the ':' prefix.
  • How to prevent the creation of a root tag, using $options['rootTag'].

This is very much like Case 03: Two SELECTs instead of a LEFT OUTER JOIN, but with a demonstration of some splecial features.

In contrast to Case 03 we want:

  • all tag names should be uppercase
  • an additional child tag for ARTIST: BIRTH_YEAR_TWO_DIGIT that will contain only the last two digets of BIRTH_YERAR
  • the ARTIST tag should have two attributes: ARTISTID and MAINTAINER set to the static value 'Lukas Feiler'.
  • the ALBUM tags should not be contained in an ALBUMS tag but should be directly within the ARTIST tag, e.g.
    1. <artist>
    2.   ...
    3.   <album>...</album>
    4.   <album>...</album>
    5. </artist>
    instead of
    1. <artist>
    2.   ...
    3.   <album>
    4.     <album>...</album>
    5.     <album>...</album>
    6.   </albums>
    7. </artist>
  • the ALBUM tag should have one attribute: ALBUMID
  • the ALBUM tag should have an additional child tag: GENRE; note that this is a column of the table artist!

case04.php

case04.php looks like this:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.     "SELECT
  7.         *
  8.      FROM
  9.         artist",
  10.     array(
  11.         'rootTag' => 'MUSIC_LIBRARY',
  12.         'rowTag' => 'ARTIST',
  13.         'idColumn' => 'artistid',
  14.         'elements' => array(
  15.             'NAME' => 'name',
  16.             'BIRTH_YEAR' => 'birth_year',
  17.             'BIRTH_YEAR_TWO_DIGIT' => "#firstTwoChars()",
  18.             'BIRTH_PLACE' => 'birth_place',
  19.             'GENRE' => 'genre',
  20.             'albums' => array(
  21.                 'sql' => array(
  22.                     'data' => array(
  23.                         'artistid'
  24.                     ),
  25.                     'query' => 'SELECT * FROM album WHERE artist_id = ?'
  26.                 ),
  27.                 'sql_options' => array(
  28.                     'merge_selective' => array('genre')
  29.                 ),
  30.                 'rootTag' => '',
  31.                 'rowTag' => 'ALBUM',
  32.                 'idColumn' => 'albumid',
  33.                 'elements' => array(
  34.                     'TITLE' => 'title',
  35.                     'PUBLISHED_YEAR' => 'published_year',
  36.                     'COMMENT' => 'comment',
  37.                     'GENRE' => 'genre'
  38.                 ),
  39.                 'attributes' => array(
  40.                     'ALBUMID' => 'albumid'
  41.                 )
  42.             )
  43.         ),
  44.         'attributes' => array(
  45.             'ARTISTID' => 'artistid',
  46.             'MAINTAINER' => ':Lukas Feiler'
  47.         )
  48.     )
  49. );
  50.  
  51. header('Content-Type: application/xml');
  52.  
  53. $dom->formatOutput true;
  54. print $dom->saveXML();
  55.  
  56. function firstTwoChars($record)
  57. {
  58.     return substr($record['birth_year']2);
  59. }
  60. ?>
Let's go over the changes:
  • as we wanted all tag names uppercased, all elements were specified like
           'TAG_NAME' => 'column_name'
           
    This is because XML_Query2XML will use the array key as the tag name if it is not numeric.
  • BIRTH_YEAR_TWO_DIGIT was specified as
           'BIRTH_YEAR_TWO_DIGIT' => "#firstTwoChars()",
           
    The prefix '#' tells XML_Query2XML that the following string is a function to call. The current record is passed as argument to that function. firstTwoChars in our case returns the first two characters of the string stored in $record['birth_year'].
  • the ARTIST tag now has two attributes: they are specified in an array using the 'attribute' option. Both use a Simple Attribute Specifications. The ARTISTID attribute simply uses the column name 'artistid'. In the MAINTAINER attribute we specify a static value. This is done by prefixing it by a colon (':'). Without the colon, XML_Query2XML would treat it as a column name.
  • the ALBUM tags are now not contained in an ALBUMS tag anymore but directly within the ARTIST tag; this is done by setting 'rootTag' to an empty string. Alternatively we just could have omitted the rootTag option.
  • ALBUM's new attribute ALBUMID is specified using the 'attribute' option.
  • ALBUM's new child tag GENRE contains the value of a column of the table artist. If we had used the sql default options we would have seen a XML_Query2XML_ConfigException with the following message:
    [elements][albums][elements][GENRE]: The column "genre" was not found in the result set.
           
    This is because the result of the first SQL query is not available at this level. As far as this level is concerned, it got overwritten with the result of our second query. But as we need both to be present, we selectively merger them using array_merge(). This is achieved by setting the sql_option 'merge_selective' to an array that contains all columns of the parent record that should also be available on the current level. As we do not have any confilicting column names, we just leave the sql_option 'merge_master' set to false which means that the results of the parent level's query is the 'master', i.e. overwrite the results from the query on this level.


case04.xml

The resulting XML data looks like this:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <MUSIC_LIBRARY>
  3.   <ARTIST ARTISTID="1" MAINTAINER="Lukas Feiler">
  4.     <NAME>Curtis Mayfield</NAME>
  5.     <BIRTH_YEAR>1920</BIRTH_YEAR>
  6.     <BIRTH_YEAR_TWO_DIGIT>20</BIRTH_YEAR_TWO_DIGIT>
  7.     <BIRTH_PLACE>Chicago</BIRTH_PLACE>
  8.     <GENRE>Soul</GENRE>
  9.     <ALBUM ALBUMID="1">
  10.       <TITLE>New World Order</TITLE>
  11.       <PUBLISHED_YEAR>1990</PUBLISHED_YEAR>
  12.       <COMMENT>the best ever!</COMMENT>
  13.       <GENRE>Soul</GENRE>
  14.     </ALBUM>
  15.     <ALBUM ALBUMID="2">
  16.       <TITLE>Curtis</TITLE>
  17.       <PUBLISHED_YEAR>1970</PUBLISHED_YEAR>
  18.       <COMMENT>that man's got somthin' to say</COMMENT>
  19.       <GENRE>Soul</GENRE>
  20.     </ALBUM>
  21.   </ARTIST>
  22.   <ARTIST ARTISTID="2" MAINTAINER="Lukas Feiler">
  23.     <NAME>Isaac Hayes</NAME>
  24.     <BIRTH_YEAR>1942</BIRTH_YEAR>
  25.     <BIRTH_YEAR_TWO_DIGIT>42</BIRTH_YEAR_TWO_DIGIT>
  26.     <BIRTH_PLACE>Tennessee</BIRTH_PLACE>
  27.     <GENRE>Soul</GENRE>
  28.     <ALBUM ALBUMID="3">
  29.       <TITLE>Shaft</TITLE>
  30.       <PUBLISHED_YEAR>1972</PUBLISHED_YEAR>
  31.       <COMMENT>he's the man</COMMENT>
  32.       <GENRE>Soul</GENRE>
  33.     </ALBUM>
  34.   </ARTIST>
  35.   <ARTIST ARTISTID="3" MAINTAINER="Lukas Feiler">
  36.     <NAME>Ray Charles</NAME>
  37.     <BIRTH_YEAR>1930</BIRTH_YEAR>
  38.     <BIRTH_YEAR_TWO_DIGIT>30</BIRTH_YEAR_TWO_DIGIT>
  39.     <BIRTH_PLACE>Mississippi</BIRTH_PLACE>
  40.     <GENRE>Country and Soul</GENRE>
  41.   </ARTIST>
  42. </MUSIC_LIBRARY>


case04_debug.php

case04_debug.php reveals nothing new compared to case03_debug.php but it's included for completeness.

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5.  
  6. require_once 'Log.php';
  7. $debugLogger Log::factory('file''case04.log''Query2XML');
  8. $query2xml->enableDebugLog($debugLogger);
  9.  
  10. $query2xml->startProfiling();
  11.  
  12.  
  13. $dom $query2xml->getXML(
  14.     "SELECT
  15.         *
  16.      FROM
  17.         artist",
  18.     array(
  19.         'rootTag' => 'MUSIC_LIBRARY',
  20.         'rowTag' => 'ARTIST',
  21.         'idColumn' => 'artistid',
  22.         'elements' => array(
  23.             'NAME' => 'name',
  24.             'BIRTH_YEAR' => 'birth_year',
  25.             'BIRTH_YEAR_TWO_DIGIT' => "#firstTwoChars()",
  26.             'BIRTH_PLACE' => 'birth_place',
  27.             'GENRE' => 'genre',
  28.             'albums' => array(
  29.                 'sql' => array(
  30.                     'data' => array(
  31.                         'artistid'
  32.                     ),
  33.                     'query' => 'SELECT * FROM album WHERE artist_id = ?'
  34.                 ),
  35.                 'sql_options' => array(
  36.                     'merge_selective' => array('genre')
  37.                 ),
  38.                 'rootTag' => '',
  39.                 'rowTag' => 'ALBUM',
  40.                 'idColumn' => 'albumid',
  41.                 'elements' => array(
  42.                     'TITLE' => 'title',
  43.                     'PUBLISHED_YEAR' => 'published_year',
  44.                     'COMMENT' => 'comment',
  45.                     'GENRE' => 'genre'
  46.                 ),
  47.                 'attributes' => array(
  48.                     'ALBUMID' => 'albumid'
  49.                 )
  50.             )
  51.         ),
  52.         'attributes' => array(
  53.             'ARTISTID' => 'artistid',
  54.             'MAINTAINER' => ':Lukas Feiler'
  55.         )
  56.     )
  57. );
  58.  
  59. header('Content-Type: application/xml');
  60.  
  61. $dom->formatOutput true;
  62. print $dom->saveXML();
  63.  
  64. require_once 'File.php';
  65. $fp new File();
  66. $fp->write('case04.profile'$query2xml->getProfile()FILE_MODE_WRITE);
  67.  
  68. function firstTwoChars($record)
  69. {
  70.     return substr($record['birth_year']2);
  71. }
  72. ?>
The lines 6-8 do the debugging, line 10 and 64-66 the profiling. This will create case04.log and case04.profile.


case04.log

The format of a debug log file is documented at Logging and Debugging XML_Query2XML. Our debug log now contains 4 queries and is exactly the same as case03.log:

Apr 18 19:01:25 Query2XML [info] QUERY: SELECT
        *
     FROM
        artist
     ORDER BY
        artistid
Apr 18 19:01:25 Query2XML [info] DONE
Apr 18 19:01:25 Query2XML [info] QUERY: SELECT * FROM album WHERE artist_id = ?; DATA:1
Apr 18 19:01:25 Query2XML [info] DONE
Apr 18 19:01:25 Query2XML [info] QUERY: SELECT * FROM album WHERE artist_id = ?; DATA:2
Apr 18 19:01:25 Query2XML [info] DONE
Apr 18 19:01:25 Query2XML [info] QUERY: SELECT * FROM album WHERE artist_id = ?; DATA:3
Apr 18 19:01:25 Query2XML [info] DONE
     


case04.profile

Profiling is essential for performance tuning. The format of the output is documented under XML_Query2XML::getProfile(). Our profile looks exactly like case03.profile:

FROM_DB FROM_CACHE CACHED AVG_DURATION DURATION_SUM SQL
1       0          false  0.0034000873 0.0034000873 SELECT
        *
     FROM
        artist
3       0          false  0.0035278797 0.0105836391 SELECT * FROM album WHERE artist_id = ?

TOTAL_DURATION: 0.081415891647339
DB_DURATION:    0.026465892791748
     


Case 05: three LEFT OUTER JOINs

Case 05 will teach you:

Case 05 is a demonstration of complex element specifications.

case05.php

case05.php looks like this:

  1. <?php
  2. require_once 'XML/Query2XML.php';
  3. require_once 'MDB2.php';
  4. $query2xml XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
  5. $dom $query2xml->getXML(
  6.     "SELECT
  7.          *
  8.      FROM
  9.          customer c
  10.          LEFT JOIN sale s ON c.customerid = s.customer_id
  11.          LEFT JOIN album al ON s.album_id = al.albumid
  12.          LEFT JOIN artist ar ON al.artist_id = ar.artistid",
  13.     array(
  14.         'rootTag' => 'music_store',
  15.         'rowTag' => 'customer',
  16.         'idColumn' => 'customerid',
  17.         'elements' => array(
  18.             'customerid',
  19.             'first_name',
  20.             'last_name',
  21.             'email',
  22.             'sales' => array(
  23.                 'rootTag' => 'sales',
  24.                 'rowTag' => 'sale',
  25.                 'idColumn' => 'saleid',
  26.                 'elements' => array(
  27.                     'saleid',
  28.                     'timestamp',
  29.                     'date' => '#Callbacks::getFirstWord()',
  30.                     'time' => '#Callbacks::getSecondWord()',
  31.                     'album' => array(
  32.                         'rootTag' => '',
  33.                         'rowTag' => 'album',
  34.                         'idColumn' => 'albumid',
  35.                         'elements' => array(
  36.                             'albumid',
  37.                             'title',
  38.                             'published_year',
  39.                             'comment',
  40.                             'artist' => array(
  41.                                 'rootTag' => '',
  42.                                 'rowTag' => 'artist',
  43.                                 'idColumn' => 'artistid',
  44.                                 'elements' => array(
  45.                                     'artistid',
  46.                                     'name',
  47.                                     'birth_year',
  48.                                     'birth_place',
  49.                                     'genre'
  50.                                 //artist elements
  51.                             //artist array
  52.                         //album elements
  53.                     //album array
  54.                 //sales elements
  55.             //sales array
  56.         //root elements
  57.     //root
  58. )//getXML method call
  59.  
  60. $root $dom->firstChild;
  61. $root->setAttribute('date_generated'date("Y-m-d\TH:i:s"1124801570));
  62.  
  63. header('Content-Type: application/xml');
  64.  
  65. $dom->formatOutput true;
  66. print $dom->saveXML(