XML_Query2XML
Generating XML data from SQL queries
Table of Contents
IntroductionXML_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.
RequirementsXML_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.xThe 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, ADOdbXML_Query2XML has drivers for the database abstraction layers PDO, PEAR MDB2, PEAR DB and ADOdb.
Using PDO with XML_Query2XML works like this:
<?php
require_once 'XML/Query2XML.php';
$pdo = new PDO('mysql://root@localhost/Query2XML_Tests');
?>
Using MDB2 with XML_Query2XML works like this:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$mdb2 = MDB2::factory('mysql://root@localhost/Query2XML_Tests');
?>
The same thing with PEAR DB looks like that:
<?php
require_once 'XML/Query2XML.php';
require_once 'DB.php';
$db = DB::connect('mysql://root@localhost/Query2XML_Tests');
?>
And again the same thing with ADOdb:
<?php
require_once 'XML/Query2XML.php';
require_once 'adodb/adodb.inc.php';
//require_once 'adodb/adodb-exceptions.inc.php';
//require_once 'adodb/adodb-pear.inc.php';
$adodb = ADONewConnection('mysql');
$adodb->Connect('localhost', 'root', '', 'Query2XML_Tests');
?>
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_LDAPSince 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:
<?php
require_once 'XML/Query2XML.php';
$ldap = Net_LDAP::connect(
'host' => 'ldap.example.com',
'port' => 389,
'version' => 3,
'starttls' => true,
'binddn' => 'cn=Manager,ou=people,dc=example,dc=com',
'bindpw' => 'secret'
);
?>
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.
$sqlThis 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):
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
if (isset ($_REQUEST['artistid']) && is_numeric($_REQUEST['artistid'])) {
$artistid = $_REQUEST['artistid'];
} else {
$artistid = 1;
}
$dom = $query2xml->getXML(
"SELECT * FROM artist WHERE artistid = $artistid",
array(
'rootTag' => 'favorite_artist',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'elements' => array(
'name',
'birth_year',
'music_genre' => 'genre'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
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.
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$artistid = $_REQUEST['artistid'];
$dom = $query2xml->getXML(
array(
'data' => array(
":$artistid"
),
'query' => 'SELECT * FROM artist WHERE artistid = ?'
),
array(
'rootTag' => 'favorite_artist',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'elements' => array(
'name',
'birth_year',
'music_genre' => 'genre'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
The resulting XML data is identical in both cases (given that artistid was submitted as 1):
<?xml version="1.0" encoding="UTF-8"?>
<favorite_artist>
<artist>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<music_genre>Soul</music_genre>
</artist>
</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:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
false,
array(
'idColumn' => false,
'rowTag' => '__tables',
'rootTag' => 'music_store',
'elements' => array(
'artists' => array(
'rootTag' => 'artists',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'sql' => 'SELECT * FROM artist',
'elements' => array(
'*'
)
),
'albums' => array(
'rootTag' => 'albums',
'rowTag' => 'album',
'idColumn' => 'albumid',
'sql' => 'SELECT * FROM album',
'elements' => array(
'*'
)
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
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:
<?xml version="1.0" encoding="UTF-8"?>
<music_store>
<artists>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
</artist>
<artist>
<artistid>2</artistid>
<name>Isaac Hayes</name>
<birth_year>1942</birth_year>
<birth_place>Tennessee</birth_place>
<genre>Soul</genre>
</artist>
<artist>
<artistid>3</artistid>
<name>Ray Charles</name>
<birth_year>1930</birth_year>
<birth_place>Mississippi</birth_place>
<genre>Country and Soul</genre>
</artist>
</artists>
<albums>
<album>
<albumid>1</albumid>
<artist_id>1</artist_id>
<title>New World Order</title>
<published_year>1990</published_year>
<comment>the best ever!</comment>
</album>
<album>
<albumid>2</albumid>
<artist_id>1</artist_id>
<title>Curtis</title>
<published_year>1970</published_year>
<comment>that man's got somthin' to say</comment>
</album>
<album>
<albumid>3</albumid>
<artist_id>2</artist_id>
<title>Shaft</title>
<published_year>1972</published_year>
<comment>he's the man</comment>
</album>
</albums>
</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 SpecificationsThese 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:
array(
'COLUMN1',
'COLUMN2'
);
This might result in XML data like this:
<COLUMN1>this was the contents of COLUMN1</COLUMN1>
<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
array(
'ELEMENT1' => 'COLUMN1',
'ELEMENT2' => 'COLUMN2'
);
This would make the same data appear like this:
<ELEMENT1>this was the contents of COLUMN1</ELEMENT1>
<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':
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'favorite_artists',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'elements' => array(
'name',
'birth_year',
'music_genre' => 'genre'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
This results in the following XML data:
<?xml version="1.0" encoding="UTF-8"?>
<favorite_artists>
<artist>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<music_genre>Soul</music_genre>
</artist>
<artist>
<name>Isaac Hayes</name>
<birth_year>1942</birth_year>
<music_genre>Soul</music_genre>
</artist>
<artist>
<name>Ray Charles</name>
<birth_year>1930</birth_year>
<music_genre>Country and Soul</music_genre>
</artist>
</favorite_artists>
The following example demonstrates the usage of all different types:
<?php
require_once 'XML/Query2XML.php';
require_once 'XML/Query2XML/Callback.php';
require_once 'MDB2.php';
class Utils
{
function trim($record, $columnName)
{
return trim($record[$columnName]);
}
function getPublishedYearCentury($record)
{
return floor($record['published_year']/100 );
}
}
class ToLowerCallback implements XML_Query2XML_Callback
{
private $_columnName = '';
public function __construct($columnName)
{
$this->_columnName = $columnName;
}
public function execute(array $record)
{
return strtolower($record[$this->_columnName]);
}
}
$dom = $query2xml->getXML(
"SELECT
*
FROM
sale,
store,
album
WHERE
sale.store_id = store.storeid
AND
sale.album_id = album.albumid
AND
sale.timestamp < '2005-06-01'",
array(
'rootTag' => 'sales',
'idColumn' => 'saleid',
'rowTag' => 'sale',
'elements' => array(
'saleid',
'sale_timestamp' => 'timestamp',
'static' => ':some static text',
'album_century' => '#Utils::getPublishedYearCentury()',
'album_title' => '?#Utils::trim(title)',
'album_comment' => new ToLowerCallback('comment'),
'storeid',
'store_building1' => '?&building_xmldata',
'store_building2' => '?=building_xmldata',
'store_building3' => '?^building_xmldata'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
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:
<?xml version="1.0" encoding="UTF-8"?>
<sales>
<sale>
<saleid>1</saleid>
<sale_timestamp>2005-05-25 07:32:00</sale_timestamp>
<static>some static text</static>
<now>1187498966</now>
<album_century>19</album_century>
<album_title>New World Order</album_title>
<album_comment>the best ever!</album_comment>
<storeid>1</storeid>
<store_building1>
<building>
<floors>4</floors>
<elevators>2</elevators>
<square_meters>3200</square_meters>
</building>
</store_building1>
<store_building2>< ![CDATA[<building><floors>4</floors><elevators>2</elevators><square_meters>3200</square_meters></building>]] ></store_building2>
<store_building3>PGJ1aWxkaW5nPjxmbG9vcnM+NDwvZmxvb3JzPjxlbGV2YXRvcnM+MjwvZWxldmF0b3JzPjxzcXVhcmVfbWV0ZXJzPjMyMDA8L3NxdWFyZV9tZXRlcnM+PC9idWlsZGluZz4=</store_building3>
</sale>
<sale>
<saleid>11</saleid>
<sale_timestamp>2005-05-25 07:23:00</sale_timestamp>
<static>some static text</static>
<now>1187498966</now>
<album_century>19</album_century>
<album_title>Curtis</album_title>
<album_comment>that man's got somthin' to say</album_comment>
<storeid>2</storeid>
<store_building1>
<building>
<floors>2</floors>
<elevators>1</elevators>
<square_meters>400</square_meters>
</building>
</store_building1>
<store_building2>< ![CDATA[<building><floors>2</floors><elevators>1</elevators><square_meters>400</square_meters></building>]] ></store_building2>
<store_building3>PGJ1aWxkaW5nPjxmbG9vcnM+MjwvZmxvb3JzPjxlbGV2YXRvcnM+MTwvZWxldmF0b3JzPjxzcXVhcmVfbWV0ZXJzPjQwMDwvc3F1YXJlX21ldGVycz48L2J1aWxkaW5nPg==</store_building3>
</sale>
</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 SpecificationsA 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':
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'favorite_artists',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'elements' => array(
'name' => array(
'value' => 'name',
'attributes' => array(
'type' => ':full_name'
)
),
'birth_year',
'music_genre' => 'genre'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
This results in the following XML data:
<?xml version="1.0" encoding="UTF-8"?>
<favorite_artists>
<artist>
<name type="full_name">Curtis Mayfield</name>
<birth_year>1920</birth_year>
<music_genre>Soul</music_genre>
</artist>
<artist>
<name type="full_name">Isaac Hayes</name>
<birth_year>1942</birth_year>
<music_genre>Soul</music_genre>
</artist>
<artist>
<name type="full_name">Ray Charles</name>
<birth_year>1930</birth_year>
<music_genre>Country and Soul</music_genre>
</artist>
</favorite_artists>
Here is another little example:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist LEFT JOIN album ON album.artist_id = artist.artistid",
array(
'rootTag' => 'music_library',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'elements' => array(
'artistid',
'name',
'birth_year',
'birth_place',
'genre',
'albums' => array(
'rootTag' => 'albums',
'rowTag' => 'album',
'idColumn' => 'albumid',
'elements' => array('albumid', 'title', 'published_year')
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
This results in the following XML data:
<?xml version="1.0" encoding="UTF-8"?>
<music_library>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
<albums>
<album>
<albumid>1</albumid>
<title>New World Order</title>
<published_year>1990</published_year>
</album>
<album>
<albumid>2</albumid>
<title>Curtis</title>
<published_year>1970</published_year>
</album>
</albums>
</artist>
<artist>
<artistid>2</artistid>
<name>Isaac Hayes</name>
<birth_year>1942</birth_year>
<birth_place>Tennessee</birth_place>
<genre>Soul</genre>
<albums>
<album>
<albumid>3</albumid>
<title>Shaft</title>
<published_year>1972</published_year>
</album>
</albums>
</artist>
<artist>
<artistid>3</artistid>
<name>Ray Charles</name>
<birth_year>1930</birth_year>
<birth_place>Mississippi</birth_place>
<genre>Country and Soul</genre>
<albums />
</artist>
</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
<artist>
<name>Curtis Mayfield</name>
<album>
<name>New World Order</name>
</album>
</artist>
<artist>
<name>Curtis Mayfield</name>
<album>
<name>Curits</name>
</album>
</artist>
but rather
<artist>
<name>Curtis Mayfield</name>
<albums>
<album>
<name>New World Order</name>
</album>
<albums>
<name>Curtis</name>
</albums>
</albums>
</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 ShortcutThe 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:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'favorite_artists',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'elements' => array(
'*'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
As the result set contains the column artistid, name, birth_year, birth_place and genre the XML data will look like this:
<?xml version="1.0" encoding="UTF-8"?>
<favorite_artists>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
</artist>
<artist>
<artistid>2</artistid>
<name>Isaac Hayes</name>
<birth_year>1942</birth_year>
<birth_place>Tennessee</birth_place>
<genre>Soul</genre>
</artist>
<artist>
<artistid>3</artistid>
<name>Ray Charles</name>
<birth_year>1930</birth_year>
<birth_place>Mississippi</birth_place>
<genre>Country and Soul</genre>
</artist>
</favorite_artists>
This is because internally, the array
'elements' => array(
'*'
)
is expanded to
'elements' => array(
'artistid',
'name',
'birth_year',
'birth_place',
'genre'
)
Think of the asterisk as a variable that will get replaced with each column name found in the result set:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'favorite_artists',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'elements' => array(
'TAG_*' => '#padWithHyphens(*)'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
function padWithHyphens($record, $columnName)
{
return '--' . $record[$columnName] . '--';
}
?>
The above code would result in the following data:
<?xml version="1.0" encoding="UTF-8"?>
<favorite_artists>
<artist>
<TAG_artistid>--1--</TAG_artistid>
<TAG_name>--Curtis Mayfield--</TAG_name>
<TAG_birth_year>--1920--</TAG_birth_year>
<TAG_birth_place>--Chicago--</TAG_birth_place>
<TAG_genre>--Soul--</TAG_genre>
</artist>
<artist>
<TAG_artistid>--2--</TAG_artistid>
<TAG_name>--Isaac Hayes--</TAG_name>
<TAG_birth_year>--1942--</TAG_birth_year>
<TAG_birth_place>--Tennessee--</TAG_birth_place>
<TAG_genre>--Soul--</TAG_genre>
</artist>
<artist>
<TAG_artistid>--3--</TAG_artistid>
<TAG_name>--Ray Charles--</TAG_name>
<TAG_birth_year>--1930--</TAG_birth_year>
<TAG_birth_place>--Mississippi--</TAG_birth_place>
<TAG_genre>--Country and Soul--</TAG_genre>
</artist>
</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:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'favorite_artists',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'elements' => array(
'*' => '*',
'genre' => '#genre2uppercase()'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
function genre2uppercase($record)
{
}
?>
The resulting XML data looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<favorite_artists>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>SOUL</genre>
</artist>
<artist>
<artistid>2</artistid>
<name>Isaac Hayes</name>
<birth_year>1942</birth_year>
<birth_place>Tennessee</birth_place>
<genre>SOUL</genre>
</artist>
<artist>
<artistid>3</artistid>
<name>Ray Charles</name>
<birth_year>1930</birth_year>
<birth_place>Mississippi</birth_place>
<genre>COUNTRY AND SOUL</genre>
</artist>
</favorite_artists>
This is because internally, the array
'elements' => array(
'*' => '*',
'genre' => '#genre2uppercase()'
)
is expanded to
'elements' => array(
'artistid',
'name',
'birth_year',
'birth_place',
'genre' => '#genre2uppercase()'
)
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:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'favorite_artists',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'elements' => array(
'*' => '*',
'genre' => array(
'value' => '#genre2uppercase()'
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
function genre2uppercase($record)
{
}
?>
If we wanted to include all columns in the XML output except "genre" we could use a little trick:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'favorite_artists',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'elements' => array(
'*' => '*',
'genre' => '?:'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
In the resulting XML data the column "genre" is missing because we used the CONDITIONAL prefix '?' in combination with a static empty text:
<?xml version="1.0" encoding="UTF-8"?>
<favorite_artists>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
</artist>
<artist>
<artistid>2</artistid>
<name>Isaac Hayes</name>
<birth_year>1942</birth_year>
<birth_place>Tennessee</birth_place>
</artist>
<artist>
<artistid>3</artistid>
<name>Ray Charles</name>
<birth_year>1930</birth_year>
<birth_place>Mississippi</birth_place>
</artist>
</favorite_artists>
The exact same result could of course also be achieved using the "condition" option:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'favorite_artists',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'elements' => array(
'*' => '*',
'genre' => array(
'condition' => '#returnFalse()'
//this would also work: 'condition' => ':'
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
function returnFalse()
{
return false;
}
?>
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:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'favorite_artists',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'elements' => array(
'tag' => '*'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
This is because expanding
'elements' => array(
'tag' => '*'
)
to
'elements' => array(
'tag' => 'artistid',
'tag' => 'name',
'tag' => 'birth_year',
'tag' => 'birth_place',
'tag' => 'genre'
)
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 KeysSometimes 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
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.
<?php
/**Returns a unique ID base on the values stored in
* $record[$columnName1] and $record[$columnName2].
*
* @param array $record An associative array.
* @param string $columnName1 The name of the first column.
* @param string $columnName2 The name of the second column.
* @return int The ID.
*/
function generateIdFromMultiKeyPK($record, $columnName1, $columnName2)
{
static $ids = array();
static $idCounter = 0;
$column1 = $record[$columnName1];
$column2 = $record[$columnName2];
if (!isset($ids[$column1])) {
$ids[$column1] = array();
}
if (!isset($ids[$column1][$column2])) {
$ids[$column1][$column2] = $idCounter++;
}
return $ids[$column1][$column2];
}
?>
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 SpecificationsIt 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':
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'favorite_artists',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'elements' => array(),
'attributes' => array(
'name',
'birth_year',
'music_genre' => 'genre'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
This results in the following XML data:
<?xml version="1.0" encoding="UTF-8"?>
<favorite_artists>
<artist name="Curtis Mayfield" birth_year="1920" music_genre="Soul"/>
<artist name="Isaac Hayes" birth_year="1942" music_genre="Soul"/>
<artist name="Ray Charles" birth_year="1930" music_genre="Country and Soul"/>
</favorite_artists>
Complex Attribute SpecificationsA 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:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'favorite_artists',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'attributes' => array(
'name',
'birth_year',
'bornBefore1940' => array(
'value' => ':true',
'condition' => '#lessThan(birth_year, 1940)'
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
function lessThan($record, $columnName, $num)
{
return $record[$columnName] < $num;
}
?>
This results in the following XML data:
<?xml version="1.0" encoding="UTF-8"?>
<favorite_artists>
<artist birth_year="1920" bornBefore1940="true" name="Curtis Mayfield" />
<artist birth_year="1942" name="Isaac Hayes" />
<artist birth_year="1930" bornBefore1940="true" name="Ray Charles" />
</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.
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'favorite_artists',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'attributes' => array(
'name',
'birth_year',
'firstAlbumTitle' => array(
'value' => 'title',
'sql' => array(
'data' => array(
'artistid'
),
'query' => "SELECT * FROM album WHERE artist_id = ? ORDER BY published_year"
)
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
This results in the following XML data:
<?xml version="1.0" encoding="UTF-8"?>
<favorite_artists>
<artist birth_year="1920" firstAlbumTitle="Curtis" name="Curtis Mayfield" />
<artist birth_year="1942" firstAlbumTitle="Shaft" name="Isaac Hayes" />
<artist birth_year="1930" name="Ray Charles" />
</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.
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'favorite_artists',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'attributes' => array(
'name',
'birth_year',
'firstAlbum' => array(
'value' => '#combineTitleAndGenre()',
'sql' => array(
'data' => array(
'artistid'
),
'query' => "SELECT * FROM album WHERE artist_id = ? ORDER BY published_year"
),
'sql_options' => array(
'merge_selective' => array('genre')
)
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
function combineTitleAndGenre($record)
{
return $record['title'] . ' (' . $record['genre'] . ')';
}
?>
This results in the following XML data:
<?xml version="1.0" encoding="UTF-8"?>
<favorite_artists>
<artist name="Curtis Mayfield" birth_year="1920" firstAlbum="Curtis (Soul)"/>
<artist name="Isaac Hayes" birth_year="1942" firstAlbum="Shaft (Soul)"/>
<artist name="Ray Charles" birth_year="1930"/>
</favorite_artists>
Using the Asterisk ShortcutThe 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:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'favorite_artists',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'attributes' => array(
'*'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
This produces this XML data:
<?xml version="1.0" encoding="UTF-8"?>
<favorite_artists>
<artist artistid="1" birth_place="Chicago" birth_year="1920" genre="Soul" name="Curtis Mayfield" />
<artist artistid="2" birth_place="Tennessee" birth_year="1942" genre="Soul" name="Isaac Hayes" />
<artist artistid="3" birth_place="Mississippi" birth_year="1930" genre="Country and Soul" name="Ray Charles" />
</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:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'favorite_artists',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'elements' => array(
'name',
'birth_year',
'genre'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
'rowTag' was set to 'artist' therefore the resulting XML data is:
<?xml version="1.0" encoding="UTF-8"?>
<favorite_artists>
<artist>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<genre>Soul</genre>
</artist>
<artist>
<name>Isaac Hayes</name>
<birth_year>1942</birth_year>
<genre>Soul</genre>
</artist>
<artist>
<name>Ray Charles</name>
<birth_year>1930</birth_year>
<genre>Country and Soul</genre>
</artist>
</favorite_artists>
Now let's have a look at a more advanced example:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT
*
FROM
artist
LEFT JOIN album ON album.artist_id = artist.artistid",
array(
'rootTag' => 'music_library',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'elements' => array(
'artistid',
'name',
'birth_year',
'birth_place',
'genre',
'albums' => array(
'rootTag' => 'albums',
'rowTag' => 'album',
'idColumn' => 'albumid',
'elements' => array(
'albumid',
'title',
'published_year',
'comment'
)
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
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:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'favorite_artists',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'elements' => array(
'name' => array(
'value' => 'name',
'attributes' => array(
'type' => ':full_name'
)
),
'birth_year',
'music_genre' => 'genre'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
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>:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM customer",
array(
'rootTag' => 'customers',
'idColumn' => 'customerid',
'rowTag' => 'customer',
'elements' => array(
'customerid',
'name_and_email' => array(
'dynamicRowTag' => 'first_name',
'value' => 'email'
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
The resulting XML looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<customers>
<customer>
<customerid>1</customerid>
<Jane>jane.doe@example.com</Jane>
</customer>
<customer>
<customerid>2</customerid>
<John>john.doe@example.com</John>
</customer>
<customer>
<customerid>3</customerid>
<Susan>susan.green@example.com</Susan>
</customer>
<customer>
<customerid>4</customerid>
<Victoria>victory.alt@example.com</Victoria>
</customer>
<customer>
<customerid>5</customerid>
<Will>will.wippy@example.com</Will>
</customer>
<customer>
<customerid>6</customerid>
<Tim>tim.raw@example.com</Tim>
</customer>
<customer>
<customerid>7</customerid>
<Nick>nick.fallow@example.com</Nick>
</customer>
<customer>
<customerid>8</customerid>
<Ed>ed.burton@example.com</Ed>
</customer>
<customer>
<customerid>9</customerid>
<Jack>jack.woo@example.com</Jack>
</customer>
<customer>
<customerid>10</customerid>
<Maria>maria.gonzales@example.com</Maria>
</customer>
</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:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'favorite_artists',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'elements' => array(
'name',
'birth_year',
'genre'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
'rootTag' was set to 'favorite_artists'. The resulting XML data therefore is:
<?xml version="1.0" encoding="UTF-8"?>
<favorite_artists>
<artist>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<genre>Soul</genre>
</artist>
<artist>
<name>Isaac Hayes</name>
<birth_year>1942</birth_year>
<genre>Soul</genre>
</artist>
<artist>
<name>Ray Charles</name>
<birth_year>1930</birth_year>
<genre>Country and Soul</genre>
</artist>
</favorite_artists>
Here goes an example with the rootTag being used at a lower level:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT
*
FROM
artist
LEFT JOIN album ON album.artist_id = artist.artistid",
array(
'rootTag' => 'music_library',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'elements' => array(
'artistid',
'name',
'birth_year',
'birth_place',
'genre',
'albums' => array(
'rootTag' => 'albums',
'rowTag' => 'album',
'idColumn' => 'albumid',
'elements' => array(
'albumid',
'title',
'published_year',
'comment'
)
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
['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:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT
*
FROM
artist
LEFT JOIN album ON album.artist_id = artist.artistid",
array(
'rootTag' => 'music_library',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'elements' => array(
'artistid',
'name',
'birth_year',
'birth_place',
'genre',
'albums' => array(
'rowTag' => 'album',
'idColumn' => 'albumid',
'elements' => array(
'albumid',
'title',
'published_year',
'comment'
)
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
The resulting XML looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<music_library>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
<album>
<albumid>1</albumid>
<title>New World Order</title>
<published_year>1990</published_year>
<comment>the best ever!</comment>
</album>
<album>
<albumid>2</albumid>
<title>Curtis</title>
<published_year>1970</published_year>
<comment>that man's got somthin' to say</comment>
</album>
</artist>
<artist>
<artistid>2</artistid>
<name>Isaac Hayes</name>
<birth_year>1942</birth_year>
<birth_place>Tennessee</birth_place>
<genre>Soul</genre>
<album>
<albumid>3</albumid>
<title>Shaft</title>
<published_year>1972</published_year>
<comment>he's the man</comment>
</album>
</artist>
<artist>
<artistid>3</artistid>
<name>Ray Charles</name>
<birth_year>1930</birth_year>
<birth_place>Mississippi</birth_place>
<genre>Country and Soul</genre>
</artist>
</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:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT
*
FROM
artist
LEFT JOIN album ON album.artist_id = artist.artistid",
array(
'rootTag' => 'music_library',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'elements' => array(
'artistid',
'name',
'birth_year',
'birth_place',
'genre',
'albums' => array(
'rootTag' => '__albums',
'rowTag' => 'album',
'idColumn' => 'albumid',
'elements' => array(
'albumid',
'title',
'published_year',
'comment'
)
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
$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.
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT
*
FROM
album al,
artist ar
WHERE
al.artist_id = ar.artistid",
array(
'rootTag' => 'albums',
'idColumn' => 'albumid',
'rowTag' => 'album',
'elements' => array(
'albumid',
'title',
'published_year',
'published_century' => array(
'value' => "#Utils::getPublishedYearCentury()",
'attributes' => array(
'digitCount' => ':2'
)
),
'comment' => array(
'value' => '?comment',
'attributes' => array(
'type' => ':short text'
)
),
'genre' => array(
'value' => "?#Utils::trimGenre()"
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
class Utils
{
function trimGenre($record)
{
return trim($record['genre']);
}
function getPublishedYearCentury($record)
{
return floor($record['published_year']/100 );
}
}
?>
The resulting XML data looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<albums>
<album>
<albumid>1</albumid>
<title>New World Order</title>
<published_year>1990</published_year>
<published_century digitCount="2">19</published_century>
<comment type="short text">the best ever!</comment>
<genre>Soul</genre>
</album>
<album>
<albumid>2</albumid>
<title>Curtis</title>
<published_year>1970</published_year>
<published_century digitCount="2">19</published_century>
<comment type="short text">that man's got somthin' to say</comment>
<genre>Soul</genre>
</album>
<album>
<albumid>3</albumid>
<title>Shaft</title>
<published_year>1972</published_year>
<published_century digitCount="2">19</published_century>
<comment type="short text">he's the man</comment>
<genre>Soul</genre>
</album>
</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:
<?php
if (isset($_REQUEST['includeCondition'])) {
$includeCondition = ($_REQUEST['includeCondition'] == '1');
} else {
$includeCondition = false;
}
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom =& $query2xml->getXML(
"SELECT
*
FROM
artist
LEFT JOIN album ON album.artist_id = artist.artistid",
array(
'rootTag' => 'music_library',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'elements' => array(
'artistid',
'name',
'birth_year',
'birth_place',
'genre',
'albums' => array(
'rootTag' => 'albums',
'rowTag' => 'album',
'idColumn' => 'albumid',
'condition' => '#isSpecialPublishedYear()',
'elements' => array(
'albumid',
'title',
'published_year',
'comment' => array(
'value' => 'comment',
'condition' => ':' . ($includeCondition ? '1' : '0')
)
)
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
/**Returns whether $year is 1970 or 1972.
*/
function isSpecialPublishedYear($record)
{
//do some highly complex calculations ...
return $record['published_year'] == 1970 || $record['published_year'] == 1972;
}
?>
The resulting XML data is:
<?xml version="1.0" encoding="UTF-8"?>
<music_library>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
<albums>
<album>
<albumid>2</albumid>
<title>Curtis</title>
<published_year>1970</published_year>
</album>
</albums>
</artist>
<artist>
<artistid>2</artistid>
<name>Isaac Hayes</name>
<birth_year>1942</birth_year>
<birth_place>Tennessee</birth_place>
<genre>Soul</genre>
<albums>
<album>
<albumid>3</albumid>
<title>Shaft</title>
<published_year>1972</published_year>
</album>
</albums>
</artist>
<artist>
<artistid>3</artistid>
<name>Ray Charles</name>
<birth_year>1930</birth_year>
<birth_place>Mississippi</birth_place>
<genre>Country and Soul</genre>
<albums />
</artist>
</music_library>
Note that (if present) $options['sql'] will
get processed *before* evaluating the condition. This allows you to wirte code
like the following:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT
*
FROM
artist",
array(
'rootTag' => 'artists',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'elements' => array(
'artistid',
'name',
'albums' => array(
'idColumn' => 'albumid',
'sql' => array(
'data' => array(
'artistid'
),
'query' => "SELECT * FROM album WHERE artist_id = ?",
),
'condition' => '#isGT1980()',
'elements' => array(
'title',
'published_year'
)
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
function isGT1980($record)
{
return $record['published_year'] > 1980;
}
?>
"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:
<?xml version="1.0" encoding="UTF-8"?>
<artists>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<albums>
<title>New World Order</title>
<published_year>1990</published_year>
</albums>
</artist>
<artist>
<artistid>2</artistid>
<name>Isaac Hayes</name>
</artist>
<artist>
<artistid>3</artistid>
<name>Ray Charles</name>
</artist>
</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 SpecificationSince v0.8.0 a simple query specifications are purely static strings (in most cases
you will want to use a Complex Query Specification):
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT
*
FROM
artist
WHERE
artistid = 1",
array(
'rootTag' => 'music_library',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'elements' => array(
'artistid',
'name',
'birth_year',
'birth_place',
'genre',
'albums' => array(
'sql' => 'SELECT * FROM album WHERE artist_id = 1',
'rootTag' => 'albums',
'rowTag' => 'album',
'idColumn' => 'albumid',
'elements' => array(
'albumid',
'title',
'published_year',
'comment'
)
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
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 SpecificationA 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:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT
*
FROM
artist",
array(
'rootTag' => 'music_library',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'elements' => array(
'artistid',
'name',
'birth_year',
'birth_place',
'genre',
'albums' => array(
'sql' => array(
'data' => array(
'artistid'
),
'query' => "SELECT * FROM album WHERE artist_id = ?"
),
'rootTag' => 'albums',
'rowTag' => 'album',
'idColumn' => 'albumid',
'elements' => array(
'albumid',
'title',
'published_year',
'comment'
)
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
$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:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT
*
FROM
artist",
array(
'rootTag' => 'MUSIC_LIBRARY',
'rowTag' => 'ARTIST',
'idColumn' => 'artistid',
'elements' => array(
'NAME' => 'name',
'BIRTH_YEAR' => 'birth_year',
'GENRE' => 'genre',
'albums' => array(
'sql' => array(
'data' => array('artistid'),
'query' => "SELECT * FROM album WHERE artist_id = ?"
),
'sql_options' => array(
'merge' => true
),
'rootTag' => '',
'rowTag' => 'ALBUM',
'idColumn' => 'albumid',
'elements' => array(
'TITLE' => 'title',
'PUBLISHED_YEAR' => 'published_year',
'COMMENT' => 'comment',
'GENRE' => 'genre'
)
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
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:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT
*
FROM
artist",
array(
'rootTag' => 'MUSIC_LIBRARY',
'rowTag' => 'ARTIST',
'idColumn' => 'artistid',
'elements' => array(
'NAME' => 'name',
'BIRTH_YEAR' => 'birth_year',
'GENRE' => 'genre',
'albums' => array(
'sql' => array(
'data' => array('artistid'),
'query' => "SELECT * FROM album WHERE artist_id = ?"
),
'sql_options' => array(
'merge_selective' => array('genre')
),
'rootTag' => '',
'rowTag' => 'ALBUM',
'idColumn' => 'albumid',
'elements' => array(
'TITLE' => 'title',
'PUBLISHED_YEAR' => 'published_year',
'COMMENT' => 'comment',
'GENRE' => 'genre'
)
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
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:
'mapper' => 'MyMapper::map'
- array('CLASS', 'STATIC_METHOD'): this syntax also allows you to use a static
method for mapping:
'mapper' => array('MyMapper', 'map')
- array($instance, 'METHOD'): this syntax allows you to use a non-static
method for mapping:
'mapper' => array($myMap, 'map')
- 'FUNCTION': this syntax allows you to use a regular function
for mapping:
'mapper' => 'myUppercaseMapper'
- false: use the boolean value false (or any other value that == false) to
deactivate any special mapping:
'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):
<?php
class SomeMapper
{
public function map($str)
{
//do something with $str
return $str;
}
}
require_once 'XML/Query2XML.php';
require_once 'XML/Query2XML/ISO9075Mapper.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML( //
"SELECT * FROM artist", //
array( //
'rootTag' => 'favorite_artists', //no mapping
'idColumn' => 'artistid', //nothing to map
'rowTag' => 'artist', //no mapping
'mapper' => 'SomeMapper::map', //
'elements' => array( //
'artistid', //mapping
'NAME' => 'name', //no mapping as the tag name is specified
'*', //mapping
'TAG_*' => '*', //does a mapping too!
'albums' => array( //nothing to map
'sql' => array( //
'data' => array( //
'artistid' //nothing to map
), //
'query' => 'SELECT * FROM album WHERE artist_id = ?' //
), //
'rootTag' => 'albums', //no mapping
'rowTag' => 'album', //no mapping
'idColumn' => 'albumid',//nothing to map
'elements' => array( //
'albumid', //mapping using the mapper specified at the root level
'title', //mapping using the mapper specified at the root level
'published_year', //mapping using the mapper specified at the root level
'comment' //mapping using the mapper specified at the root level
) //
) //
), //
'attributes' => array( //
'artistid', //mapping
'NAME' => 'name', //no mapping as the tag name is specified
'*', //mapping
'TAG_*' => '*' //does a mapping too!
) //
) //
); //
header('Content-Type: application/xml'); //
print $dom->saveXML(); //
?>
Mapping SQL identifiers to XML names in accordance with ISO/IEC 9075-14:2005The 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:
<?php
require_once 'XML/Query2XML.php';
require_once 'XML/Query2XML/ISO9075Mapper.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'favorite_artists',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'mapper' => 'XML_Query2XML_ISO9075Mapper::map',
'elements' => array(
'*'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
Building your own mappersThere 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:
<?php
require_once 'XML/Query2XML.php';
require_once 'XML/Query2XML/ISO9075Mapper.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'favorite_artists',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'elements' => array(
'NAME' => 'name',
'BIRTH_YEAR' => 'birth_year',
'BIRTH_PLACE' => 'birth_place',
'GENRE' => 'genre',
),
'attributes' => array(
'ARTISTID' => 'artistid'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
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:
<?php
class UppercaseMapper
{
public function map($str)
{
}
}
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'favorite_artists',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'mapper' => 'UppercaseMapper::map',
'elements' => array(
'name',
'birth_year',
'birth_place',
'genre',
),
'attributes' => array(
'artistid'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
The resulting XML data looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<favorite_artists>
<artist ARTISTID="1">
<NAME>Curtis Mayfield</NAME>
<BIRTH_YEAR>1920</BIRTH_YEAR>
<BIRTH_PLACE>Chicago</BIRTH_PLACE>
<GENRE>Soul</GENRE>
</artist>
<artist ARTISTID="2">
<NAME>Isaac Hayes</NAME>
<BIRTH_YEAR>1942</BIRTH_YEAR>
<BIRTH_PLACE>Tennessee</BIRTH_PLACE>
<GENRE>Soul</GENRE>
</artist>
<artist ARTISTID="3">
<NAME>Ray Charles</NAME>
<BIRTH_YEAR>1930</BIRTH_YEAR>
<BIRTH_PLACE>Mississippi</BIRTH_PLACE>
<GENRE>Country and Soul</GENRE>
</artist>
</favorite_artists>
Using multiple mappersLet'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:
<?php
class MyMappers
{
public function uppercaseMapper($str)
{
}
public function lowercaseMapper($str)
{
}
}
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT
*
FROM
artist",
array(
'rootTag' => 'music_library',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'mapper' => 'MyMappers::uppercaseMapper',
'elements' => array(
'*',
'albums' => array(
'sql' => array(
'data' => array(
'artistid'
),
'query' => 'SELECT * FROM album WHERE artist_id = ?'
),
'rootTag' => 'albums',
'rowTag' => 'album',
'idColumn' => 'albumid',
'mapper' => 'MyMappers::lowercaseMapper',
'elements' => array(
'*',
'artist_id' => '?:'
)
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
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':
<?php
class MyMappers
{
public function uppercaseMapper($str)
{
}
}
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT
*
FROM
artist",
array(
'rootTag' => 'music_library',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'mapper' => 'MyMappers::uppercaseMapper',
'elements' => array(
'*',
'albums' => array(
'sql' => array(
'data' => array(
'artistid'
),
'query' => 'SELECT * FROM album WHERE artist_id = ?'
),
'rootTag' => 'albums',
'rowTag' => 'album',
'idColumn' => 'albumid',
'mapper' => false,
'elements' => array(
'*',
'artist_id' => '?:'
)
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
In both cases the resulting XML data will look like this:
<?xml version="1.0" encoding="UTF-8"?>
<music_library>
<artist>
<ARTISTID>1</ARTISTID>
<NAME>Curtis Mayfield</NAME>
<BIRTH_YEAR>1920</BIRTH_YEAR>
<BIRTH_PLACE>Chicago</BIRTH_PLACE>
<GENRE>Soul</GENRE>
<albums>
<album>
<albumid>1</albumid>
<title>New World Order</title>
<published_year>1990</published_year>
<comment>the best ever!</comment>
</album>
<album>
<albumid>2</albumid>
<title>Curtis</title>
<published_year>1970</published_year>
<comment>that man's got somthin' to say</comment>
</album>
</albums>
</artist>
<artist>
<ARTISTID>2</ARTISTID>
<NAME>Isaac Hayes</NAME>
<BIRTH_YEAR>1942</BIRTH_YEAR>
<BIRTH_PLACE>Tennessee</BIRTH_PLACE>
<GENRE>Soul</GENRE>
<albums>
<album>
<albumid>3</albumid>
<title>Shaft</title>
<published_year>1972</published_year>
<comment>he's the man</comment>
</album>
</albums>
</artist>
<artist>
<ARTISTID>3</ARTISTID>
<NAME>Ray Charles</NAME>
<BIRTH_YEAR>1930</BIRTH_YEAR>
<BIRTH_PLACE>Mississippi</BIRTH_PLACE>
<GENRE>Country and Soul</GENRE>
<albums />
</artist>
</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:
'encoder' => 'MyEncoder::encode'
- array('CLASS', 'STATIC_METHOD'): this syntax also allows you to use a static
method for encoding:
'encoder' => array('MyEncoder', 'encode')
- array($instance, 'METHOD'): this syntax allows you to use a non-static
method for encoding:
'encoder' => array($myEncoder, 'encode')
- 'FUNCTION': this syntax allows you to use a regular function
for encoding:
'encoder' => 'myUTF8toISO88591Encoder'
- false: use the boolean value false to deactivate encoding:
'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().
'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):
<?php
class SomeEncoder
{
public function encode($str)
{
//do something with $str
return $str;
}
}
require_once 'XML/Query2XML.php';
require_once 'XML/Query2XML/ISO9075Mapper.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'favorite_artists',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'encoder' => 'SomeEncoder::encode', /* we define an encoder at the root level */
'elements' => array(
'artistid', // encoding will be
'name', // performed on these
'albums' => array(
'sql' => array(
'data' => array(
'artistid'
),
'query' => 'SELECT * FROM album WHERE artist_id = ?'
),
'rootTag' => 'albums',
'rowTag' => 'album',
'idColumn' => 'albumid',
'elements' => array(
'albumid', // encoder setting is affective on all lower
'title' // levels
),
'attributes' => array(
'comment' // note: encoding is also performed for attributes
)
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
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:
'encoder' => null
UTF-8 to ISO-8859-1 encodingIf 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():
'encoder' => 'utf8_decode'
or define a wrapper for mb_convert_encoding()
and use that:
function utf8ToLatin1($str)
{
//hint: mb_convert_encoding (str, to_encoding, from_encoding)
}
specified as encoder:
'encoder' => 'utf8ToLatin1'
Disabling encodingIf 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:
'encoder' => false
Using multiple encodersIt 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.
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT
*
FROM
artist
LEFT JOIN album ON album.artist_id = artist.artistid",
array(
'rootTag' => 'music_library',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'elements' => array( // all columns of the table artist are in
'artistid', // ISO-8859-1; the default conversion therefore
'name', // is just fine
'birth_year',
'birth_place',
'genre',
'albums' => array(
'rootTag' => 'albums',
'rowTag' => 'album',
'idColumn' => 'albumid',
'encoder' => false, // the columns of the album table already are in UTF-8;
'elements' => array( // we therefore have to disable encoding
'albumid',
'title',
'published_year',
'comment'
)
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
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:
<?php
function latin1ToUTF8($str)
{
// alternatively we could have used
// return mb_convert_encoding($str, 'UTF-8', 'iso-8859-1');
}
function utf16ToUTF8($str)
{
}
function windows1252ToUTF8($str)
{
}
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT
*
FROM
artist
LEFT JOIN album ON album.artist_id = artist.artistid",
array(
'rootTag' => 'music_library',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'elements' => array(
'artistid',
'name', // name is in ISO-8859-1 and therefore will be handled by the default conversion
'birth_year',
'birth_place',
'genre' => array(
'value' => 'genre',
'encoder' => false // genre already is in UTF-8
),
'albums' => array(
'rootTag' => 'albums',
'rowTag' => 'album',
'idColumn' => 'albumid',
'elements' => array(
'albumid',
'title' => array(
'value' => 'title',
'encoder' => 'utf16ToUTF8' // title is in UTF-16 and therefore needs
), // special treatment
'published_year'
),
'attributes' => array(
'comment' => array(
'value' => 'comment',
'encoder' => 'windows1252ToUTF8' // comment is in Windows-1252
)
)
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
Modifying the returned DOMDocument instanceXML_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()):
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$doc = $query2xml->getXML(
"SELECT
*
FROM
artist",
array(
'rootTag' => 'music_library',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'elements' => array(
'artistid',
'name',
'birth_year',
'birth_place',
'genre'
)
)
);
$root = $doc->firstChild;
$root->setAttribute('copyright', 'John Doe 2007');
header('Content-Type: application/xml');
$doc->formatOutput = true;
print $doc->saveXML();
?>
This adds an attribute named 'copyright' with a value of 'John Doe 2007'
to the root element <music_library>:
<?xml version="1.0" encoding="UTF-8"?>
<music_library copyright="John Doe 2007">
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
</artist>
<artist>
<artistid>2</artistid>
<name>Isaac Hayes</name>
<birth_year>1942</birth_year>
<birth_place>Tennessee</birth_place>
<genre>Soul</genre>
</artist>
<artist>
<artistid>3</artistid>
<name>Ray Charles</name>
<birth_year>1930</birth_year>
<birth_place>Mississippi</birth_place>
<genre>Country and Soul</genre>
</artist>
</music_library>
Final Notes on XML_Query2XML::getXML()You might also want to read the API docs: XML_Query2XML.
Integrating other XML data sourcesIntroductionSince 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
'&:<name>John Doe</name>'
or if you wanted to unserialize a string returned from a callback function, you would use
'&#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:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom =& $query2xml->getXML(
"SELECT
*, NULL AS additional_xml
FROM
store",
array(
'rootTag' => 'music_stores',
'rowTag' => 'store',
'idColumn' => 'storeid',
'elements' => array(
'storeid',
'country',
'state',
'city',
'street',
'building_xmldata' => '&building_xmldata',
'additional_xml' => '&additional_xml',
)
)
);
$dom->formatOutput = true;
print $dom->saveXML();
?>
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.
<?xml version="1.0" encoding="UTF-8"?>
<music_stores>
<store>
<storeid>1</storeid>
<country>US</country>
<state>New York</state>
<city>New York</city>
<street>Broadway & 72nd Str</street>
<building_xmldata>
<building>
<floors>4</floors>
<elevators>2</elevators>
<square_meters>3200</square_meters>
</building>
</building_xmldata>
<additional_xml/>
</store>
<store>
<storeid>2</storeid>
<country>US</country>
<state>New York</state>
<city>Larchmont</city>
<street>Palmer Ave 71</street>
<building_xmldata>
<building>
<floors>2</floors>
<elevators>1</elevators>
<square_meters>400</square_meters>
</building>
</building_xmldata>
<additional_xml/>
</store>
</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
'building_xmldata' => '&building_xmldata'
you would write
'building_xmldata' => array(
'value' => '&building_xmldata'
)
- 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 (?):
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom =& $query2xml->getXML(
"SELECT
*, NULL AS additional_xml
FROM
store",
array(
'rootTag' => 'music_stores',
'rowTag' => 'store',
'idColumn' => 'storeid',
'elements' => array(
'storeid',
'country',
'state',
'city',
'street',
'building_xmldata' => '?&building_xmldata',
'additional_xml' => '?&additional_xml',
)
)
);
$dom->formatOutput = true;
print $dom->saveXML();
?>
The resulting XML data shows that the unserialized XML is still enclosed
by a <building_xmldata> tag but the <additional_xml> elements are gone:
<?xml version="1.0" encoding="UTF-8"?>
<music_stores>
<store>
<storeid>1</storeid>
<country>US</country>
<state>New York</state>
<city>New York</city>
<street>Broadway & 72nd Str</street>
<building_xmldata>
<building>
<floors>4</floors>
<elevators>2</elevators>
<square_meters>3200</square_meters>
</building>
</building_xmldata>
</store>
<store>
<storeid>2</storeid>
<country>US</country>
<state>New York</state>
<city>Larchmont</city>
<street>Palmer Ave 71</street>
<building_xmldata>
<building>
<floors>2</floors>
<elevators>1</elevators>
<square_meters>400</square_meters>
</building>
</building_xmldata>
</store>
</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
'building_xmldata' => '?&building_xmldata'
you would write
'building_xmldata' => array(
'value' => '?&building_xmldata'
)
- 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:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom =& $query2xml->getXML(
"SELECT
*
FROM
store",
array(
'rootTag' => 'music_stores',
'rowTag' => 'store',
'idColumn' => 'storeid',
'elements' => array(
'storeid',
'country',
'state',
'city',
'street',
'__building_xmldata' => '&building_xmldata'
)
)
);
$dom->formatOutput = true;
print $dom->saveXML();
?>
The resulting XML now does not contain <building_xmldata> tags that surround
the <building> elements:
<?xml version="1.0" encoding="UTF-8"?>
<music_stores>
<store>
<storeid>1</storeid>
<country>US</country>
<state>New York</state>
<city>New York</city>
<street>Broadway & 72nd Str</street>
<building>
<floors>4</floors>
<elevators>2</elevators>
<square_meters>3200</square_meters>
</building>
</store>
<store>
<storeid>2</storeid>
<country>US</country>
<state>New York</state>
<city>Larchmont</city>
<street>Palmer Ave 71</street>
<building>
<floors>2</floors>
<elevators>1</elevators>
<square_meters>400</square_meters>
</building>
</store>
</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
'__building_xmldata' => '&building_xmldata'
you would write
'__building_xmldata' => array(
'value' => '&building_xmldata'
)
or (which is effectively the same)
'building_xmldata' => array(
'rowTag' => '__building_xmldata'
'value' => '&building_xmldata'
)
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:
'__building_xmldata' => '?&building_xmldata'
Writing your own unserialization methodIf 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
$doc = new DOMDocument();
if (!@$doc->loadXML($xmlData)) {
'Could not unserialize the following XML data: '
. $ret
);
}
return $doc->documentElement;
} else {
return null;
}
Returning DOMNode instances from callbacksIf 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.
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom =& $query2xml->getXML(
"SELECT
*
FROM
album",
array(
'rootTag' => 'music_store',
'rowTag' => 'album',
'idColumn' => 'albumid',
'elements' => array(
'albumid',
'title',
'time' => '#getTime()'
)
)
);
$dom->formatOutput = true;
print $dom->saveXML();
function getTime()
{
$dom = new DOMDocument();
$unixtime = $dom->createElement('unixtime');
$unixtime->appendChild ($dom->createTextNode (time()));
return $unixtime;
}
?>
Have a look at the resulting XML data:
<?xml version="1.0" encoding="UTF-8"?>
<music_store>
<album>
<albumid>1</albumid>
<title>New World Order</title>
<time>
<unixtime>1167167461</unixtime>
</time>
</album>
<album>
<albumid>2</albumid>
<title>Curtis</title>
<time>
<unixtime>1167167461</unixtime>
</time>
</album>
<album>
<albumid>3</albumid>
<title>Shaft</title>
<time>
<unixtime>1167167461</unixtime>
</time>
</album>
</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 '__':
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom =& $query2xml->getXML(
"SELECT
*
FROM
album",
array(
'rootTag' => 'music_store',
'rowTag' => 'album',
'idColumn' => 'albumid',
'elements' => array(
'albumid',
'title',
'__time' => '#getTime()'
)
)
);
$dom->formatOutput = true;
print $dom->saveXML();
function getTime()
{
$dom = new DOMDocument();
$unixtime = $dom->createElement('unixtime');
$unixtime->appendChild ($dom->createTextNode (time()));
$rfc2822date = $dom->createElement('rfc2822date');
$rfc2822date->appendChild ($dom->createTextNode (date('r')));
return array($unixtime, $rfc2822date);
}
?>
The surrounding <time> element is now gone and we have both tags <unixtime>
and <rfc2822date>:
<?xml version="1.0" encoding="UTF-8"?>
<music_store>
<album>
<albumid>1</albumid>
<title>New World Order</title>
<unixtime>1167169325</unixtime>
<rfc2822date>Tue, 26 Dec 2006 22:42:05 +0100</rfc2822date>
</album>
<album>
<albumid>2</albumid>
<title>Curtis</title>
<unixtime>1167169325</unixtime>
<rfc2822date>Tue, 26 Dec 2006 22:42:05 +0100</rfc2822date>
</album>
<album>
<albumid>3</albumid>
<title>Shaft</title>
<unixtime>1167169325</unixtime>
<rfc2822date>Tue, 26 Dec 2006 22:42:05 +0100</rfc2822date>
</album>
</music_store>
Exception HandlingThe 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:
<?php
require_once 'XML/Query2XML.php';
try {
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'favorite_artist',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'elements' => array(
'name',
'birth_year',
'genre'
)
)
);
echo $dom->saveXML();
/*
* log this exceptions
* display some error message that does not disclose sensitive information
*/
}
?>
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:
<?php
require_once 'XML/Query2XML.php';
try {
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'favorite_artist',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'elements' => array(
'name',
'birth_year',
'genre'
)
)
);
echo $dom->saveXML();
//handle DB error
//handle XML error
/*
* Handle all other errors/exceptions; this will not only catch
* XML_Query2XML_ConfigException but also all other exceptions that might be
* added in future releases of XML_Query2XML.
*/
}
?>
Bottom line: make sure you at least have a catch block for XML_Query2XML_Exception.
Output formattingBefore calling the saveXML() method on your DOMDocument instance set its public property
formatOutput to true!
Here goes an example:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'favorite_artist',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'elements' => array(
'name',
'birth_year',
'genre'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
Alternatively you could also use PEAR XML_Beautifier.
Here goes an example:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'favorite_artist',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'elements' => array(
'name',
'birth_year',
'genre'
)
)
);
header('Content-Type: application/xml');
print '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
require_once 'XML/Beautifier.php';
$beautifier = new XML_Beautifier();
print $beautifier->formatString($dom->saveXML());
?>
Output CachingIf 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:
<?php
require_once('Cache/Lite.php');
/*
* Set a id for this cache; if you generate the XML data based
* on values passed via POST/GET/COOKIE, include these values
* in $id. This does not need to be an integer; it's md5sum
* will be used.
*/
$id = '123';
//set a few options
$options = array(
'cacheDir' => "/tmp/",
'lifeTime' => 3600
);
//create a Cache_Lite object
$cache = new Cache_Lite($options);
// content type for xml data
header('Content-Type: application/xml');
//test if there is a valide cache for this id
if ($data = $cache->get($id)) {
print $data;
} else {
//no valid cache found
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(...);
$dom->formatOutput = true;
//cache the XML data
$data = $dom->saveXML();
$cache->save($data, $id);
print $data;
}
?>
For more information on PEAR Cache_Lite, please see the
Cache_Lite manual.
XML encodingIt 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.
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT
*
FROM
artist",
array(
'rootTag' => 'music_library',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'encoder' => false, // disable default ISO-8859-1 to UTF-8 encoding
'elements' => array(
'artistid',
'name',
'birth_year',
'birth_place',
'genre'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
$dom->encoding = 'iso-8859-1'; //setting XML encoding
print $dom->saveXML();
?>
This results in the following XML:
<?xml version="1.0" encoding="iso-8859-1"?>
<music_library>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
</artist>
<artist>
<artistid>2</artistid>
<name>Isaac Hayes</name>
<birth_year>1942</birth_year>
<birth_place>Tennessee</birth_place>
<genre>Soul</genre>
</artist>
<artist>
<artistid>3</artistid>
<name>Ray Charles</name>
<birth_year>1930</birth_year>
<birth_place>Mississippi</birth_place>
<genre>Country and Soul</genre>
</artist>
</music_library>
Handling Binary DataIf 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:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$mdb2 = MDB2::factory('mysql://root@localhost/Query2XML_Tests');
$dom = $query2xml->getXML(
'SELECT * FROM album',
array(
'idColumn' => 'albumid',
'rowTag' => 'album',
'rootTag' => 'music_store',
'elements' => array(
'albumid',
'title',
'comment' => '^comment'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
The resulting XML data looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<music_store>
<album>
<albumid>1</albumid>
<title>New World Order</title>
<comment>dGhlIGJlc3QgZXZlciE=</comment>
</album>
<album>
<albumid>2</albumid>
<title>Curtis</title>
<comment>dGhhdCBtYW4ncyBnb3Qgc29tdGhpbicgdG8gc2F5</comment>
</album>
<album>
<albumid>3</albumid>
<title>Shaft</title>
<comment>aGUncyB0aGUgbWFu</comment>
</album>
</music_store>
Using dynamic $options to dump all data of your databaseFor 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:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$mdb2 = MDB2::factory('mysql://root@localhost/Query2XML_Tests');
//we need MDB2's manager module to get the list of tables in a database independent way
$mdb2->loadModule('Manager');
$elements = array();
$mdb2->setOption('portability', MDB2_PORTABILITY_NONE);
$tables = $mdb2->listTables();
$mdb2->setOption('portability', MDB2_PORTABILITY_ALL);
for ($i = 0; $i < count($tables); $i++ ) {
$elements['table' . $i] = array(
'rowTag' => 'table',
'attributes' => array(
'name' => ':' . $tables[$i]
),
'elements' => array(
'record' => array(
'idColumn' => false,
'sql' => 'SELECT * FROM ' . $tables[$i],
'elements' => array(
'*'
)
)
)
);
}
$dom = $query2xml->getXML(
false,
array(
'idColumn' => false,
'rowTag' => '__tables',
'rootTag' => 'database',
'elements' => $elements
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
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 PrefixesYou 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.
<?php
/**All command objects have to implement the interface XML_Query2XML_Callback.
*/
require_once 'XML/Query2XML/Callback.php';
/**All command classes that work on a single column value will extend this abstract class.
* Classes extending this class will have to implement the execute(array $record) method
* defined by the XML_Query2XML_Callback interface.
*/
abstract class Callback_SingleColumn implements XML_Query2XML_Callback
{
/**The column name
* @var string
*/
protected $_column = '';
/**Constructor
* @param string $column The name of the column this instance will work on.
*/
public function __construct($column)
{
$this->_column = $column;
}
/**Get the value of the column passed to the constructor.
* @param array $record An associative array as it will be passed
* to the execute(array $record) method.
* @throws XML_Query2XML_Exception If the column name passed
* to the constructor was not found in $record.
*/
protected function _getColumnValue(array $record)
{
if (array_key_exists($this->_column, $record)) {
return $record[$this->_column];
}
throw new XML_Query2XML_Exception(
'Column ' . $this->_column . ' was not found in the result set'
);
}
}
/**Use an instance of this class to get the base64 encoded value of a column.
*/
class Callback_Base64 extends Callback_SingleColumn
{
/**Called by XML_Query2XML for every record.
* @param array $record An associative array.
* @return string
*/
public function execute(array $record)
{
return base64_encode($this->_getColumnValue($record));
}
}
/**Use an instance of this class to unserialize XML data stored in a column.
*/
class Callback_Unserialization extends Callback_SingleColumn
{
/**Called by XML_Query2XML for every record.
* @param array $record An associative array.
* @return DOMElement
* @throws XML_Query2XML_XMLException If unserialization fails.
*/
public function execute(array $record)
{
$doc = new DOMDocument();
$xml = $this->_getColumnValue($record);
if (!@$doc->loadXML($xml)) {
'Could not unserialize the following XML data: '
. $xml
);
}
return $doc->documentElement;
}
}
/**Use an instance of this class to place a CDATA section around the value of a column.
*/
class Callback_CDATA extends Callback_SingleColumn
{
/**Called by XML_Query2XML for every record.
* @param array $record An associative array.
* @return DOMCDATASection
*/
public function execute(array $record)
{
$doc = new DOMDocument();
return $doc->createCDATASection($this->_getColumnValue($record));
}
}
/**Use an instance of this class to return a static data.
*/
{
/**The static data
* @var mixed
*/
private $_data = null;
/**Constructor
* @param mixed $data The static date to return for every record.
*/
public function __construct($data)
{
$this->_data = $data;
}
/**Called by XML_Query2XML for every record.
* This method will always return the same data, no matter what
* is passed as $record.
*
* @param array $record An associative array.
* @return mixed
*/
public function execute(array $record)
{
return $this->_data;
}
}
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$query2xml = XML_Query2XML::factory(MDB2::factory('mysql://root@localhost/Query2XML_Tests'));
$xml = $query2xml->getXML(
'SELECT * FROM store',
array(
'rootTag' => 'stores',
'rowTag' => 'store',
'idColumn' => 'storeid',
'elements' => array(
'base64' => new Callback_Base64('building_xmldata'),
'cdata' => new Callback_CDATA('building_xmldata'),
'static' => new Callback_StaticData('my static data'),
'unserialized' => new Callback_Unserialization('building_xmldata')
)
)
);
$xml->formatOutput = true;
print $xml->saveXML();
?>
Note that the above code is equivalent to
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$xml = $query2xml->getXML(
'SELECT * FROM store',
array(
'rootTag' => 'stores',
'rowTag' => 'store',
'idColumn' => 'storeid',
'elements' => array(
'base64' => '?^building_xmldata',
'cdata' => '=building_xmldata',
'static' => ':my static data',
'unserialized' => '&building_xmldata'
)
)
);
$xml->formatOutput = true;
print $xml->saveXML();
?>
Both produce the following XML data:
<?xml version="1.0" encoding="UTF-8"?>
<stores>
<store>
<base64>PGJ1aWxkaW5nPjxmbG9vcnM+NDwvZmxvb3JzPjxlbGV2YXRvcnM+MjwvZWxldmF0b3JzPjxzcXVhcmVfbWV0ZXJzPjMyMDA8L3NxdWFyZV9tZXRlcnM+PC9idWlsZGluZz4=</base64>
<cdata>< ![CDATA[<building><floors>4</floors><elevators>2</elevators><square_meters>3200</square_meters></building>]] ></cdata>
<static>my static data</static>
<unserialized>
<building>
<floors>4</floors>
<elevators>2</elevators>
<square_meters>3200</square_meters>
</building>
</unserialized>
</store>
<store>
<base64>PGJ1aWxkaW5nPjxmbG9vcnM+MjwvZmxvb3JzPjxlbGV2YXRvcnM+MTwvZWxldmF0b3JzPjxzcXVhcmVfbWV0ZXJzPjQwMDwvc3F1YXJlX21ldGVycz48L2J1aWxkaW5nPg==</base64>
<cdata>< ![CDATA[<building><floors>2</floors><elevators>1</elevators><square_meters>400</square_meters></building>]] ></cdata>
<static>my static data</static>
<unserialized>
<building>
<floors>2</floors>
<elevators>1</elevators>
<square_meters>400</square_meters>
</building>
</unserialized>
</store>
</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 PrefixesYou 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.)
XML_Query2XML_Data_Processor:
<?php
require_once 'XML/Query2XML.php';
require_once 'XML/Query2XML/Data/Processor.php';
require_once 'MDB2.php';
class Year2UnixTime extends XML_Query2XML_Data_Processor
{
/**
* Create a new instance of this class.
*
* @param mixed $preProcessor The pre-processor to be used. An instance of
* XML_Query2XML_Data or null.
* @param string $configPath The configuration path within the $options
* array.
*
* @return XML_Query2XML_Data_Processor_Base64
*/
public function create($preProcessor, $configPath)
{
$processor = new Year2UnixTime($preProcessor);
// The create() method of every class extending
// XML_Query2XML_Data_Processor has to call
// setConfigPath() manually!
$processor->setConfigPath($configPath);
return $processor;
}
/**
* Called by XML_Query2XML for every record in the result set.
*
* @param array $record An associative array.
*
* @return string The base64-encoded version the string returned
* by the pre-processor.
* @throws XML_Query2XML_ConfigException If the pre-processor returns
* something that cannot be converted to a string
* (i.e. an object or an array).
*/
public function execute(array $record)
{
$data = $this->runPreProcessor($record);
if (is_array($data) || is_object($data)) {
$this->getConfigPath()
. ': XML_Query2XML_Data_Processor_Base64: string '
. 'expected from pre-processor, but ' . gettype($data) . ' returned.'
);
}
return DateTime::createFromFormat('Y-m-d H:i:s', $data . '-01-01 00:00:00', new DateTimeZone('Etc/GMT+0'))->format('U');
}
}
$query2xml->registerPrefix(, 'Year2UnixTime');
$dom = $query2xml->getXML(
"SELECT * FROM artist",
array(
'rootTag' => 'artists',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'elements' => array(
'name',
'birth_year',
'birth_year_in_unix_time' =>
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
The resulting XML data looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<artists>
<artist>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_year_in_unix_time>-1577923200</birth_year_in_unix_time>
</artist>
<artist>
<name>Isaac Hayes</name>
<birth_year>1942</birth_year>
<birth_year_in_unix_time>-883612800</birth_year_in_unix_time>
</artist>
<artist>
<name>Ray Charles</name>
<birth_year>1930</birth_year>
<birth_year_in_unix_time>-1262304000</birth_year_in_unix_time>
</artist>
</artists>
Global OptionsGlobal 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:
$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:
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_prefixAll 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:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom =& $query2xml->getXML(
"SELECT
*
FROM
album",
array(
'rootTag' => 'music_store',
'rowTag' => 'album',
'idColumn' => 'albumid',
'elements' => array(
'albumid',
'title',
'__info' => '&:<name>John Doe</name>'
)
)
);
$dom->formatOutput = true;
print $dom->saveXML();
?>
Instead of
'__info' => '&:<name>John Doe</name>'
we also could have written
'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.
<?xml version="1.0" encoding="UTF-8"?>
<music_store>
<album>
<albumid>1</albumid>
<title>New World Order</title>
<name>John Doe</name>
</album>
<album>
<albumid>2</albumid>
<title>Curtis</title>
<name>John Doe</name>
</album>
<album>
<albumid>3</albumid>
<title>Shaft</title>
<name>John Doe</name>
</album>
</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:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$query2xml->setGlobalOption('hidden_container_prefix', '___');
$dom =& $query2xml->getXML(
"SELECT
*
FROM
album",
array(
'rootTag' => 'music_store',
'rowTag' => 'album',
'idColumn' => 'albumid',
'elements' => array(
'albumid',
'title',
'__info' => '&:<name>John Doe</name>', //will not be hidden
'___info' => '&:<name>John Doe</name>' //will be hidden
)
)
);
$dom->formatOutput = true;
print $dom->saveXML();
?>
This produces the following XML data:
<?xml version="1.0" encoding="UTF-8"?>
<music_store>
<album>
<albumid>1</albumid>
<title>New World Order</title>
<__info>
<name>John Doe</name>
</__info>
<name>John Doe</name>
</album>
<album>
<albumid>2</albumid>
<title>Curtis</title>
<__info>
<name>John Doe</name>
</__info>
<name>John Doe</name>
</album>
<album>
<albumid>3</albumid>
<title>Shaft</title>
<__info>
<name>John Doe</name>
</__info>
<name>John Doe</name>
</album>
</music_store>
Logging and Debugging XML_Query2XMLIf 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:
<?php
require_once 'XML/Query2XML.php';
//create a new instance of PEAR::Log
require_once 'Log.php';
$debugLogger = Log::factory('file', 'debug.log', 'XML_Query2XML');
//start debugging
$query2xml->enableDebugLog($debugLogger);
$dom = $query2xml->getXML(
"SELECT
*
FROM
artist
ORDER BY
artistid",
array(
'rootTag' => 'music_library',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'elements' => array(
'name',
'albums' => array(
'sql' => array(
'data' => array(
'artistid'
),
'query' => 'SELECT * FROM album WHERE artist_id = ?'
),
'rowTag' => 'album',
'idColumn' => 'albumid',
'elements' => array(
'title'
)
)
)
)
);
print $dom->saveXML();
?>
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:
class MyLogger {
private $_logger = null;
public function __construct($logger)
{
$this->_logger = $logger;
}
public function log($msg)
{
}
}
Using this wrapper like this:
$query2xml->enableDebugLog(
new MyLogger(
Log::factory('file', 'debug.log', 'XML_Query2XML')
)
);
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 TuningWhen 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.
ExampleXML_Query2XML::startProfiling() should be the first and
XML_Query2XML::getProfile() the last method you call on your XML_Query2XML instance.
<?php
require_once 'XML/Query2XML.php';
//start the profiling as soon as possible
$query2xml->startProfiling();
//do the real work
$dom = $query2xml->getXML(...);
print $dom->saveXML();
//save the profile in a separate file
require_once 'File.php';
$fp = new File();
$fp->write('/tmp/query2xml_profile.txt', $query2xml->getProfile(), FILE_MODE_WRITE);
?>
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:
$this->_profile = array(
'queries' => array(),
'stop' => 0,
'duration' => 0,
'dbStop' => 0,
'dbDuration' => 0
);
The element 'queries' is itself an associative array that
uses $sql as the array key;:
$this->_profile['queries'][$sql] = array(
'fromDB' => 0,
'fromCache' => 0,
'cached' => false,
'runTimes' => array()
);
The element 'runTimes' is an indexed array that stores multiple
arrays that have the following format:
array ('start' => microtime(true ), 'stop' => 0 );
XML_Query2XML::clearProfile()Please see the API docs at XML_Query2XML::clearProfile().
The LDAP DriverSince 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 SpecificationAll LDAP queries have to be specified as an array.
LDAP query specifications can be used
for $sql and
$options['sql']. They look as follows:
array(
'data' => array(...), // optional; only relevant if placeholders are used in 'base' or 'filter'
'base' => 'ou=peopole,dc=example,dc=com',
'filter' => '(objectclass=inetOrgPerson)',
'options' => array(
'attributes' => array(
...
), ...
)
)
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.
array(
'base' => 'ou=peopole,dc=example,dc=com',
'filter' => '(objectclass=inetOrgPerson)',
'options' => array(
'attributes' => array(
'cn',
'mobile'
)
)
)
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.
<?php
require_once 'XML/Query2XML.php';
require_once 'Net/LDAP.php';
/*
* just pass an instance of Net_LDAP to the factory method
* as you would do it with an MDB2 or PDO instance.
*/
$dom = $query2xml->getXML(
array( //this is different than what you're used to from the other drivers
'base' => 'ou=people,dc=example,dc=com',
'filter' => '(objectclass=inetOrgPerson)',
'options' => array(
'attributes' => array( //to tell the LDAP driver which columns to watch out for
'cn',
'mobile',
)
)
),
array(
'rootTag' => 'employees',
'idColumn' => 'cn',
'rowTag' => 'employee',
'elements' => array(
'cn',
'mobile'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
This would produce the following XML data:
<?xml version="1.0" encoding="UTF-8"?>
<employees>
<employee>
<cn>John Doe</cn>
<mobile>666-777-888</mobile>
</employee>
<employee>
<cn>Jane Doe</cn>
<mobile>555-777-888</mobile>
</employee>
</employees>
An LDAP Query Specification that makes use of placeholders and the option 'data' looks like this:
array(
'data' => array(':John Doe'),
'base' => 'ou=peopole,dc=example,dc=com',
'filter' => '(&(objectclass=inetOrgPerson)(cn=?))',
'options' => array(
'attributes' => array(
'cn',
'mobile'
),
'query2xml_placeholder' => '?'
)
)
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.
<?php
require_once 'XML/Query2XML.php';
require_once 'Net/LDAP.php';
$dom = $query2xml->getXML(
array(
'data' => array(':' . $_GET['cn']),
'base' => 'ou=people,dc=example,dc=com',
'filter' => '(&(objectclass=inetOrgPerson)(cn=*?*))', //the question mark will be replace
//with the contents of $_GET['cn']
'options' => array(
'attributes' => array( //to tell the LDAP driver which columns to watch out for
'cn',
'mobile',
)
)
),
array(
'rootTag' => 'employees',
'idColumn' => 'cn',
'rowTag' => 'employee',
'elements' => array(
'cn',
'mobile'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
if $_GET['cn'] was set to 'John' the following XML data would be produced:
<?xml version="1.0" encoding="UTF-8"?>
<employees>
<employee>
<cn>John Doe</cn>
<mobile>666-777-888</mobile>
</employee>
</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:
<?php
require_once 'XML/Query2XML.php';
require_once 'Net/LDAP.php';
/*
* just pass an instance of Net_LDAP to the factory method
* as you would do it with an MDB2 or PDO instance.
*/
$dom = $query2xml->getXML(
array(
'base' => 'ou=people,dc=example,dc=com',
'filter' => '(objectclass=inetOrgPerson)'
//we completely omit 'options' here
),
array(
'rootTag' => 'employees',
'idColumn' => 'cn',
'rowTag' => 'employee',
'elements' => array(
'cn',
'pager'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
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 AttributesIn 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:
<?php
require_once 'XML/Query2XML.php';
require_once 'Net/LDAP.php';
$dom = $query2xml->getXML(
array(
'base' => 'ou=people,dc=example,dc=com',
'filter' => '(objectclass=inetOrgPerson)',
'options' => array(
'attributes' => array( //to tell the LDAP driver which columns to watch out for
'cn',
'mail',
)
)
),
array(
'rootTag' => 'employees',
'idColumn' => 'cn',
'rowTag' => 'employee',
'elements' => array(
'cn', //simple element specification
'mail' //simple element specification
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
notice how the resulting XML only shows the first value of the mail attribute:
<?xml version="1.0" encoding="UTF-8"?>
<employees>
<employee>
<cn>John Doe</cn>
<mail>john@example.com</mail>
</employee>
<employee>
<cn>Jane Doe</cn>
<mail>jane@example.com</mail>
</employee>
</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']:
<?php
require_once 'XML/Query2XML.php';
require_once 'Net/LDAP.php';
$dom = $query2xml->getXML(
array(
'base' => 'ou=people,dc=example,dc=com',
'filter' => '(objectclass=inetOrgPerson)',
'options' => array(
'attributes' => array( //to tell the LDAP driver which columns to watch out for
'cn',
'mail',
)
)
),
array(
'rootTag' => 'employees',
'idColumn' => 'cn',
'rowTag' => 'employee',
'elements' => array(
'cn', //simple element specification
'mail' => array(
'idColumn' => 'mail',
'value' => 'mail'
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
This produces the following XML data:
<?xml version="1.0" encoding="UTF-8"?>
<employees>
<employee>
<cn>John Doe</cn>
<mail>john@example.com</mail>
<mail>johndoe@example.com</mail>
<mail>john.doe@example.com</mail>
</employee>
<employee>
<cn>Jane Doe</cn>
<mail>jane@example.com</mail>
<mail>jane.doe@example.com</mail>
</employee>
</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 DriversThere 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:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
'SELECT * FROM artist',
array(
'rootTag' => 'artists',
'idColumn' => 'artistid',
'rowTag' => 'artist',
'elements' => array(
'name',
'genre',
'album' => array(
'sql' => array(
'driver' => $pgsqlDriver,
'data' => array(
'artistid'
),
'query' => 'SELECT * FROM album WHERE artist_id = ?'
),
'idColumn' => 'albumid',
'elements' => array(
'title',
'published_year'
)
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
The resulting XML being:
<?xml version="1.0" encoding="UTF-8"?>
<artists>
<artist>
<name>Curtis Mayfield</name>
<genre>Soul</genre>
<album>
<title>New World Order</title>
<published_year>1990</published_year>
</album>
<album>
<title>Curtis</title>
<published_year>1970</published_year>
</album>
</artist>
<artist>
<name>Isaac Hayes</name>
<genre>Soul</genre>
<album>
<title>Shaft</title>
<published_year>1972</published_year>
</album>
</artist>
<artist>
<name>Ray Charles</name>
<genre>Country and Soul</genre>
</artist>
</artists>
Now let's combine an RDBMS with the data from an LDAP server:
<?php
require_once 'XML/Query2XML.php';
require_once 'Net/LDAP.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT * FROM employee",
array(
'rootTag' => 'employees',
'idColumn' => 'employeeid',
'rowTag' => 'employee',
'elements' => array(
'name' => 'employeename',
'contact_details' => array(
'query' => array(
'data' => array(
'employeename'
),
'base' => 'ou=people,dc=example,dc=com',
'filter' => '(&(objectclass=inetOrgPerson)(cn=?))',
'options' => array(
'attributes' => array(
'cn',
'telephoneNumber',
'mobile',
'mail',
'labeledURI'
)
)
),
'idColumn' => 'cn',
'elements' => array(
'telephoneNumber' => array(
'idColumn' => 'telephoneNumber',
'value' => 'telephoneNumber'
),
'mobile' => array(
'idColumn' => 'mobile',
'value' => 'mobile'
),
'mail' => array(
'idColumn' => 'mail',
'value' => 'mail'
),
'labeledURI' => array(
'idColumn' => 'labeledURI',
'value' => 'labeledURI'
)
)
)
),
)
);
$dom->formatOutput = true;
print $dom->saveXML();
?>
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:
<?xml version="1.0" encoding="UTF-8"?>
<employees>
<employee>
<name>Michael Jones</name>
</employee>
<employee>
<name>Susi Weintraub</name>
<contact_details>
<telephoneNumber>555-111-222</telephoneNumber>
<mobile>555-666-777</mobile>
<mobile>555-777-888</mobile>
<mail>susi@example.com</mail>
<labeledURI>http://susi.example.com</labeledURI>
<labeledURI>http://susiweintraub.example.com</labeledURI>
<labeledURI>http://susi.weintraub.example.com</labeledURI>
</contact_details>
</employee>
<employee>
<name>Steve Hack</name>
</employee>
<employee>
<name>Joan Kerr</name>
</employee>
<employee>
<name>Marcus Roth</name>
</employee>
<employee>
<name>Jack Mack</name>
</employee>
<employee>
<name>Rita Doktor</name>
</employee>
<employee>
<name>David Til</name>
</employee>
<employee>
<name>Pia Eist</name>
</employee>
<employee>
<name>Hanna Poll</name>
</employee>
<employee>
<name>Jim Wells</name>
<contact_details>
<telephoneNumber>555-444-888</telephoneNumber>
<mobile>555-666-777</mobile>
<mail>jim@example.com</mail>
<mail>jim.wells@example.com</mail>
<mail>jimwells@example.com</mail>
<mail>jwells@example.com</mail>
<labeledURI>http://jimwells.example.com</labeledURI>
<labeledURI>http://jim.wells.example.com</labeledURI>
</contact_details>
</employee>
<employee>
<name>Sandra Wilson</name>
</employee>
</employees>
Implementing 1:N and N:N relationshipsOften 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 JOINThe 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:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
'SELECT
*
FROM
artist, album
WHERE
album.artist_id = artist.artistid',
array(
'rootTag' => 'music_library',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'elements' => array(
'artistid',
'name',
'albums' => array(
'rootTag' => 'albums',
'rowTag' => 'album',
'idColumn' => 'albumid',
'elements' => array(
'albumid',
'title'
)
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
And the resulting XML data looks as follows:
<?xml version="1.0" encoding="UTF-8"?>
<music_library>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<albums>
<album>
<albumid>1</albumid>
<title>New World Order</title>
</album>
<album>
<albumid>2</albumid>
<title>Curtis</title>
</album>
</albums>
</artist>
<artist>
<artistid>2</artistid>
<name>Isaac Hayes</name>
<albums>
<album>
<albumid>3</albumid>
<title>Shaft</title>
</album>
</albums>
</artist>
</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']:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
'SELECT * FROM artist',
array(
'rootTag' => 'music_library',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'elements' => array(
'artistid',
'name',
'albums' => array(
'rootTag' => 'albums',
'rowTag' => 'album',
'idColumn' => 'albumid',
'sql' => array(
'data' => array(
'artistid'
),
'query' => 'SELECT * FROM album WHERE artist_id = ?'
),
'elements' => array(
'albumid',
'title'
)
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
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.
<?xml version="1.0" encoding="UTF-8"?>
<music_library>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<albums>
<album>
<albumid>1</albumid>
<title>New World Order</title>
</album>
<album>
<albumid>2</albumid>
<title>Curtis</title>
</album>
</albums>
</artist>
<artist>
<artistid>2</artistid>
<name>Isaac Hayes</name>
<albums>
<album>
<albumid>3</albumid>
<title>Shaft</title>
</album>
</albums>
</artist>
<artist>
<artistid>3</artistid>
<name>Ray Charles</name>
<albums/>
</artist>
</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
array(
'albumid',
'title'
);
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'].
<?xml version="1.0" encoding="UTF-8"?>
<music_library>
<artist>
<name>Curtis Mayfield</name>
<album>New World Order</album>
<album>Curtis</album>
</artist>
<artist>
<name>Isaac Hayes</name>
<album>Shaft</album>
</artist>
</music_library>
The code to generate the above XML would look like this:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
'SELECT
*
FROM
artist, album
WHERE
album.artist_id = artist.artistid',
array(
'rootTag' => 'music_library',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'elements' => array(
'name',
'album' => array(
'idColumn' => 'albumid',
'value' => 'title'
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
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:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
'SELECT * FROM artist',
array(
'rootTag' => 'music_library',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'elements' => array(
'name',
'album' => array(
'sql' => array(
'data' => array(
'artistid'
),
'query' => 'SELECT * FROM album WHERE artist_id = ?'
),
'idColumn' => 'albumid',
'value' => 'title'
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
The resulting XML is similar to the one generated above. There is just
one difference: artists that have no albums are included as well:
<?xml version="1.0" encoding="UTF-8"?>
<music_library>
<artist>
<name>Curtis Mayfield</name>
<album>New World Order</album>
<album>Curtis</album>
</artist>
<artist>
<name>Isaac Hayes</name>
<album>Shaft</album>
</artist>
<artist>
<name>Ray Charles</name>
</artist>
</music_library>
A last word on JOINsAs 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 DriverIf 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():
/**Contains the abstract class XML_Query2XML_Driver and the
* exception class XML_Query2XML_DBException.
*/
require_once 'XML/Query2XML.php';
{
/*This method is defined as an abstract method within
* XML_Query2XML_Driver and therefore has to be implemented.
*/
public function getAllRecords($sql, $configPath)
{
}
}
?>
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:
<?php
public function __construct($database, $username, $password)
{
$this->_dbh = @ibase_pconnect($database, $username, $password);
if ($this->_dbh === false) {
'Could not connect to database: ' . ibase_errmsg()
. '(error code: ' . ibase_errcode() . ')'
);
}
}
?>
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:
<?php
public function getAllRecords($sql, $configPath)
{
if (!isset($sql['data'])) {
//do not presume that $sql['data'] is present
$sql['data'] = array();
}
//prepare
$statement = @ibase_prepare($this->_dbh, $sql['query']);
//execute
$args = $sql['data'];
//fetch all records
while ($record = ibase_fetch_assoc($result, IBASE_TEXT)) {
$records[] = $record;
}
ibase_free_result($result);
return $records;
}
?>
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.
<?php
/**Contains the abstract class XML_Query2XML_Driver and the
* exception class XML_Query2XML_DBException.
*/
require 'XML/Query2XML.php';
{
private $_dhb = null;
public function __construct($database, $username, $password)
{
$this->_dbh = @ibase_pconnect($database, $username, $password);
if ($this->_dbh === false) {
'Could not connect to database: ' . ibase_errmsg()
. '(error code: ' . ibase_errcode() . ')'
);
}}
/**Returns are records retrieved by running an SQL query.
* @param mixed $sql A query string or an array with the elements
* 'query' (and optionally 'data').
* @param string $configPath Where in $options the query was defined.
* @return array All records as a two-dimensional array.
*/
public function &getAllRecords($sql, $configPath)
{
if (!isset($sql['data'])) {
//do not presume that $sql['data'] is present
$sql['data'] = array();
}
/*
* prepare
*/
$statement = @ibase_prepare($this->_dbh, $sql['query']);
if ($statement === false) {
/*
* Note how we use $configPath here. This will make
* the exception message tell you where in $options
* the error needs to be fixed.
*/
$configPath . ': Could not prepare query "' . $sql['query'] . '": '
. ibase_errmsg() . '(error code: ' . ibase_errcode() . ')'
);
}
/*
* execute
*/
$args = $sql['data'];
'ibase_execute',
$args
);
if ($result === false) {
/*
* Note again how we use $configPath here.
*/
$configPath . ': Could not execute query: "' . $sql['query'] . '": '
. ibase_errmsg() . '(error code: ' . ibase_errcode() . ')'
);
} elseif ($result === true) {
//empty result set
$records = array();
} else {
//fetch all records
while ($record = ibase_fetch_assoc($result, IBASE_TEXT)) {
$records[] = $record;
}
ibase_free_result($result);
}
/*
* return a two dimensional array: numeric indexes in the first
* and associative arrays in the second dimension
*/
return $records;
}
}
//test the driver directly
$driver = new MyFirebirdDriver('localhost:/tmp/test.fdb', 'SYSDBA', 'test');
$records = $driver->getAllRecords(
array(
'data' => array(1),
'query' => 'SELECT * FROM test WHERE id = ?'
),
'[config]'
);
?>
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:
<?php
/**Exception for MyDriver errors
*/
{
/**Constructor
* @param string $message The error message.
*/
public function __construct($message)
{
parent::__construct($message);
}
}
?>
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 StudiesNow 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 getFlatXMLCase 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<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getFlatXML(
"SELECT
*
FROM
artist",
'music_library',
'artist');
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
case01.xmlThe result looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<music_library>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
</artist>
<artist>
<artistid>2</artistid>
<name>Isaac Hayes</name>
<birth_year>1942</birth_year>
<birth_place>Tennessee</birth_place>
<genre>Soul</genre>
</artist>
<artist>
<artistid>3</artistid>
<name>Ray Charles</name>
<birth_year>1930</birth_year>
<birth_place>Mississippi</birth_place>
<genre>Country and Soul</genre>
</artist>
</music_library>
Case 02: LEFT OUTER JOINCase 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.phpcase02.php looks like this:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT
*
FROM
artist
LEFT JOIN album ON album.artist_id = artist.artistid",
array(
'rootTag' => 'music_library',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'elements' => array(
'artistid',
'name',
'birth_year',
'birth_place',
'genre',
'albums' => array(
'rootTag' => 'albums',
'rowTag' => 'album',
'idColumn' => 'albumid',
'elements' => array(
'albumid',
'title',
'published_year',
'comment'
)
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
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:
case02.xmlThe resulting XML data looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<music_library>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
<albums>
<album>
<albumid>1</albumid>
<title>New World Order</title>
<published_year>1990</published_year>
<comment>the best ever!</comment>
</album>
<album>
<albumid>2</albumid>
<title>Curtis</title>
<published_year>1970</published_year>
<comment>that man's got somthin' to say</comment>
</album>
</albums>
</artist>
<artist>
<artistid>2</artistid>
<name>Isaac Hayes</name>
<birth_year>1942</birth_year>
<birth_place>Tennessee</birth_place>
<genre>Soul</genre>
<albums>
<album>
<albumid>3</albumid>
<title>Shaft</title>
<published_year>1972</published_year>
<comment>he's the man</comment>
</album>
</albums>
</artist>
<artist>
<artistid>3</artistid>
<name>Ray Charles</name>
<birth_year>1930</birth_year>
<birth_place>Mississippi</birth_place>
<genre>Country and Soul</genre>
<albums />
</artist>
</music_library>
case02_debug.phpXML_Query2XML::getXML() allows us to debug and to profile the operation.
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
require_once 'Log.php';
$debugLogger = Log::factory('file', 'case02.log', 'Query2XML');
$query2xml->enableDebugLog($debugLogger);
$query2xml->startProfiling();
$dom = $query2xml->getXML(
"SELECT
*
FROM
artist
LEFT JOIN album ON album.artist_id = artist.artistid",
array(
'rootTag' => 'music_library',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'elements' => array(
'artistid',
'name',
'birth_year',
'birth_place',
'genre',
'albums' => array(
'rootTag' => 'albums',
'rowTag' => 'album',
'idColumn' => 'albumid',
'elements' => array(
'albumid',
'title',
'published_year',
'comment'
)
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
require_once 'File.php';
$fp = new File();
$fp->write('case02.profile', $query2xml->getProfile(), FILE_MODE_WRITE);
?>
The lines 5-7 do the debugging, line 9 and 50-52 the profiling. This will create
case02.log and case02.profile.
case02.logThe 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.profileProfiling 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 JOINCase 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.phpcase03.php looks like this:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT
*
FROM
artist",
array(
'rootTag' => 'music_library',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'elements' => array(
'artistid',
'name',
'birth_year',
'birth_place',
'genre',
'albums' => array(
'sql' => array(
'data' => array(
'artistid'
),
'query' => 'SELECT * FROM album WHERE artist_id = ?'
),
'rootTag' => 'albums',
'rowTag' => 'album',
'idColumn' => 'albumid',
'elements' => array(
'albumid',
'title',
'published_year',
'comment'
)
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
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.xmlThe resulting XML data looks exactly like case02.xml:
<?xml version="1.0" encoding="UTF-8"?>
<music_library>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
<albums>
<album>
<albumid>1</albumid>
<title>New World Order</title>
<published_year>1990</published_year>
<comment>the best ever!</comment>
</album>
<album>
<albumid>2</albumid>
<title>Curtis</title>
<published_year>1970</published_year>
<comment>that man's got somthin' to say</comment>
</album>
</albums>
</artist>
<artist>
<artistid>2</artistid>
<name>Isaac Hayes</name>
<birth_year>1942</birth_year>
<birth_place>Tennessee</birth_place>
<genre>Soul</genre>
<albums>
<album>
<albumid>3</albumid>
<title>Shaft</title>
<published_year>1972</published_year>
<comment>he's the man</comment>
</album>
</albums>
</artist>
<artist>
<artistid>3</artistid>
<name>Ray Charles</name>
<birth_year>1930</birth_year>
<birth_place>Mississippi</birth_place>
<genre>Country and Soul</genre>
<albums />
</artist>
</music_library>
case03_debug.phpcase03_debug.php is similar to case02_debug.php:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
require_once 'Log.php';
$debugLogger = Log::factory('file', 'case03.log', 'Query2XML');
$query2xml->enableDebugLog($debugLogger);
$query2xml->startProfiling();
$dom = $query2xml->getXML(
"SELECT
*
FROM
artist",
array(
'rootTag' => 'music_library',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'elements' => array(
'artistid',
'name',
'birth_year',
'birth_place',
'genre',
'albums' => array(
'sql' => array(
'data' => array(
'artistid'
),
'query' => 'SELECT * FROM album WHERE artist_id = ?'
),
'rootTag' => 'albums',
'rowTag' => 'album',
'idColumn' => 'albumid',
'elements' => array(
'albumid',
'title',
'published_year',
'comment'
)
)
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
require_once 'File.php';
$fp = new File();
$fp->write('case03.profile', $query2xml->getProfile(), FILE_MODE_WRITE);
?>
The lines 6-8 do the debugging, line 10 and 54-56 the profiling. This will create
case03.log and case03.profile.
case03.logThe 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.profileProfiling 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 moreCase 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.
<artist>
...
<album>...</album>
<album>...</album>
</artist>
instead of
<artist>
...
<album>
<album>...</album>
<album>...</album>
</albums>
</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.phpcase04.php looks like this:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT
*
FROM
artist",
array(
'rootTag' => 'MUSIC_LIBRARY',
'rowTag' => 'ARTIST',
'idColumn' => 'artistid',
'elements' => array(
'NAME' => 'name',
'BIRTH_YEAR' => 'birth_year',
'BIRTH_YEAR_TWO_DIGIT' => "#firstTwoChars()",
'BIRTH_PLACE' => 'birth_place',
'GENRE' => 'genre',
'albums' => array(
'sql' => array(
'data' => array(
'artistid'
),
'query' => 'SELECT * FROM album WHERE artist_id = ?'
),
'sql_options' => array(
'merge_selective' => array('genre')
),
'rootTag' => '',
'rowTag' => 'ALBUM',
'idColumn' => 'albumid',
'elements' => array(
'TITLE' => 'title',
'PUBLISHED_YEAR' => 'published_year',
'COMMENT' => 'comment',
'GENRE' => 'genre'
),
'attributes' => array(
'ALBUMID' => 'albumid'
)
)
),
'attributes' => array(
'ARTISTID' => 'artistid',
'MAINTAINER' => ':Lukas Feiler'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
{
return substr($record['birth_year'], 2 );
}
?>
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.xmlThe resulting XML data looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<MUSIC_LIBRARY>
<ARTIST ARTISTID="1" MAINTAINER="Lukas Feiler">
<NAME>Curtis Mayfield</NAME>
<BIRTH_YEAR>1920</BIRTH_YEAR>
<BIRTH_YEAR_TWO_DIGIT>20</BIRTH_YEAR_TWO_DIGIT>
<BIRTH_PLACE>Chicago</BIRTH_PLACE>
<GENRE>Soul</GENRE>
<ALBUM ALBUMID="1">
<TITLE>New World Order</TITLE>
<PUBLISHED_YEAR>1990</PUBLISHED_YEAR>
<COMMENT>the best ever!</COMMENT>
<GENRE>Soul</GENRE>
</ALBUM>
<ALBUM ALBUMID="2">
<TITLE>Curtis</TITLE>
<PUBLISHED_YEAR>1970</PUBLISHED_YEAR>
<COMMENT>that man's got somthin' to say</COMMENT>
<GENRE>Soul</GENRE>
</ALBUM>
</ARTIST>
<ARTIST ARTISTID="2" MAINTAINER="Lukas Feiler">
<NAME>Isaac Hayes</NAME>
<BIRTH_YEAR>1942</BIRTH_YEAR>
<BIRTH_YEAR_TWO_DIGIT>42</BIRTH_YEAR_TWO_DIGIT>
<BIRTH_PLACE>Tennessee</BIRTH_PLACE>
<GENRE>Soul</GENRE>
<ALBUM ALBUMID="3">
<TITLE>Shaft</TITLE>
<PUBLISHED_YEAR>1972</PUBLISHED_YEAR>
<COMMENT>he's the man</COMMENT>
<GENRE>Soul</GENRE>
</ALBUM>
</ARTIST>
<ARTIST ARTISTID="3" MAINTAINER="Lukas Feiler">
<NAME>Ray Charles</NAME>
<BIRTH_YEAR>1930</BIRTH_YEAR>
<BIRTH_YEAR_TWO_DIGIT>30</BIRTH_YEAR_TWO_DIGIT>
<BIRTH_PLACE>Mississippi</BIRTH_PLACE>
<GENRE>Country and Soul</GENRE>
</ARTIST>
</MUSIC_LIBRARY>
case04_debug.phpcase04_debug.php reveals nothing new compared to
case03_debug.php but it's included for
completeness.
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
require_once 'Log.php';
$debugLogger = Log::factory('file', 'case04.log', 'Query2XML');
$query2xml->enableDebugLog($debugLogger);
$query2xml->startProfiling();
$dom = $query2xml->getXML(
"SELECT
*
FROM
artist",
array(
'rootTag' => 'MUSIC_LIBRARY',
'rowTag' => 'ARTIST',
'idColumn' => 'artistid',
'elements' => array(
'NAME' => 'name',
'BIRTH_YEAR' => 'birth_year',
'BIRTH_YEAR_TWO_DIGIT' => "#firstTwoChars()",
'BIRTH_PLACE' => 'birth_place',
'GENRE' => 'genre',
'albums' => array(
'sql' => array(
'data' => array(
'artistid'
),
'query' => 'SELECT * FROM album WHERE artist_id = ?'
),
'sql_options' => array(
'merge_selective' => array('genre')
),
'rootTag' => '',
'rowTag' => 'ALBUM',
'idColumn' => 'albumid',
'elements' => array(
'TITLE' => 'title',
'PUBLISHED_YEAR' => 'published_year',
'COMMENT' => 'comment',
'GENRE' => 'genre'
),
'attributes' => array(
'ALBUMID' => 'albumid'
)
)
),
'attributes' => array(
'ARTISTID' => 'artistid',
'MAINTAINER' => ':Lukas Feiler'
)
)
);
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
require_once 'File.php';
$fp = new File();
$fp->write('case04.profile', $query2xml->getProfile(), FILE_MODE_WRITE);
{
return substr($record['birth_year'], 2 );
}
?>
The lines 6-8 do the debugging, line 10 and 64-66 the profiling. This will create
case04.log and case04.profile.
case04.logThe 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.profileProfiling 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 JOINsCase 05 will teach you:
Case 05 is a demonstration of complex element specifications.
case05.phpcase05.php looks like this:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
$dom = $query2xml->getXML(
"SELECT
*
FROM
customer c
LEFT JOIN sale s ON c.customerid = s.customer_id
LEFT JOIN album al ON s.album_id = al.albumid
LEFT JOIN artist ar ON al.artist_id = ar.artistid",
array(
'rootTag' => 'music_store',
'rowTag' => 'customer',
'idColumn' => 'customerid',
'elements' => array(
'customerid',
'first_name',
'last_name',
'email',
'sales' => array(
'rootTag' => 'sales',
'rowTag' => 'sale',
'idColumn' => 'saleid',
'elements' => array(
'saleid',
'timestamp',
'date' => '#Callbacks::getFirstWord()',
'time' => '#Callbacks::getSecondWord()',
'album' => array(
'rootTag' => '',
'rowTag' => 'album',
'idColumn' => 'albumid',
'elements' => array(
'albumid',
'title',
'published_year',
'comment',
'artist' => array(
'rootTag' => '',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'elements' => array(
'artistid',
'name',
'birth_year',
'birth_place',
'genre'
) //artist elements
) //artist array
) //album elements
) //album array
) //sales elements
) //sales array
) //root elements
) //root
); //getXML method call
$root = $dom->firstChild;
$root->setAttribute ('date_generated', date("Y-m-d\TH:i:s", 1124801570 ));
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
class Callbacks
{
function getFirstWord($record)
{
return substr($record['timestamp'], 0 , strpos($record['timestamp'], ' '));
}
function getSecondWord($record)
{
return substr($record['timestamp'], strpos($record['timestamp'], ' ') + 1 );
}
}
?>
['elements']['sales']['elements']['date'] and ['time'] contain portions of the timestamp column.
Also note that a separate call to DOMNode::setAttribute() is used to
set the attribute date_generated in the root element.
case05.xmlThe resulting XML data looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<music_store date_generated="2005-08-23T14:52:50">
<customer>
<customerid>1</customerid>
<first_name>Jane</first_name>
<last_name>Doe</last_name>
<email>jane.doe@example.com</email>
<sales>
<sale>
<saleid>1</saleid>
<timestamp>2005-05-25 16:32:00</timestamp>
<date>2005-05-25</date>
<time>16:32:00</time>
<album>
<albumid>1</albumid>
<title>New World Order</title>
<published_year>1990</published_year>
<comment>the best ever!</comment>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
<sale>
<saleid>11</saleid>
<timestamp>2005-05-25 16:23:00</timestamp>
<date>2005-05-25</date>
<time>16:23:00</time>
<album>
<albumid>2</albumid>
<title>Curtis</title>
<published_year>1970</published_year>
<comment>that man's got somthin' to say</comment>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
</sales>
</customer>
<customer>
<customerid>2</customerid>
<first_name>John</first_name>
<last_name>Doe</last_name>
<email>john.doe@example.com</email>
<sales>
<sale>
<saleid>2</saleid>
<timestamp>2005-06-05 12:56:00</timestamp>
<date>2005-06-05</date>
<time>12:56:00</time>
<album>
<albumid>1</albumid>
<title>New World Order</title>
<published_year>1990</published_year>
<comment>the best ever!</comment>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
<sale>
<saleid>16</saleid>
<timestamp>2005-06-05 12:56:12</timestamp>
<date>2005-06-05</date>
<time>12:56:12</time>
<album>
<albumid>3</albumid>
<title>Shaft</title>
<published_year>1972</published_year>
<comment>he's the man</comment>
<artist>
<artistid>2</artistid>
<name>Isaac Hayes</name>
<birth_year>1942</birth_year>
<birth_place>Tennessee</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
</sales>
</customer>
<customer>
<customerid>3</customerid>
<first_name>Susan</first_name>
<last_name>Green</last_name>
<email>susan.green@example.com</email>
<sales>
<sale>
<saleid>3</saleid>
<timestamp>2005-07-10 11:03:00</timestamp>
<date>2005-07-10</date>
<time>11:03:00</time>
<album>
<albumid>1</albumid>
<title>New World Order</title>
<published_year>1990</published_year>
<comment>the best ever!</comment>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
<sale>
<saleid>12</saleid>
<timestamp>2005-07-10 11:56:00</timestamp>
<date>2005-07-10</date>
<time>11:56:00</time>
<album>
<albumid>2</albumid>
<title>Curtis</title>
<published_year>1970</published_year>
<comment>that man's got somthin' to say</comment>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
</sales>
</customer>
<customer>
<customerid>4</customerid>
<first_name>Victoria</first_name>
<last_name>Alt</last_name>
<email>victory.alt@example.com</email>
<sales>
<sale>
<saleid>4</saleid>
<timestamp>2005-07-10 10:03:00</timestamp>
<date>2005-07-10</date>
<time>10:03:00</time>
<album>
<albumid>1</albumid>
<title>New World Order</title>
<published_year>1990</published_year>
<comment>the best ever!</comment>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
<sale>
<saleid>17</saleid>
<timestamp>2005-07-10 10:03:32</timestamp>
<date>2005-07-10</date>
<time>10:03:32</time>
<album>
<albumid>3</albumid>
<title>Shaft</title>
<published_year>1972</published_year>
<comment>he's the man</comment>
<artist>
<artistid>2</artistid>
<name>Isaac Hayes</name>
<birth_year>1942</birth_year>
<birth_place>Tennessee</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
</sales>
</customer>
<customer>
<customerid>5</customerid>
<first_name>Will</first_name>
<last_name>Rippy</last_name>
<email>will.wippy@example.com</email>
<sales>
<sale>
<saleid>5</saleid>
<timestamp>2005-07-10 13:03:00</timestamp>
<date>2005-07-10</date>
<time>13:03:00</time>
<album>
<albumid>1</albumid>
<title>New World Order</title>
<published_year>1990</published_year>
<comment>the best ever!</comment>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
<sale>
<saleid>13</saleid>
<timestamp>2005-07-10 13:12:00</timestamp>
<date>2005-07-10</date>
<time>13:12:00</time>
<album>
<albumid>2</albumid>
<title>Curtis</title>
<published_year>1970</published_year>
<comment>that man's got somthin' to say</comment>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
</sales>
</customer>
<customer>
<customerid>6</customerid>
<first_name>Tim</first_name>
<last_name>Raw</last_name>
<email>tim.raw@example.com</email>
<sales>
<sale>
<saleid>6</saleid>
<timestamp>2005-07-10 14:03:00</timestamp>
<date>2005-07-10</date>
<time>14:03:00</time>
<album>
<albumid>1</albumid>
<title>New World Order</title>
<published_year>1990</published_year>
<comment>the best ever!</comment>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
<sale>
<saleid>18</saleid>
<timestamp>2005-07-10 14:03:52</timestamp>
<date>2005-07-10</date>
<time>14:03:52</time>
<album>
<albumid>3</albumid>
<title>Shaft</title>
<published_year>1972</published_year>
<comment>he's the man</comment>
<artist>
<artistid>2</artistid>
<name>Isaac Hayes</name>
<birth_year>1942</birth_year>
<birth_place>Tennessee</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
</sales>
</customer>
<customer>
<customerid>7</customerid>
<first_name>Nick</first_name>
<last_name>Fallow</last_name>
<email>nick.fallow@example.com</email>
<sales>
<sale>
<saleid>7</saleid>
<timestamp>2005-07-10 15:03:00</timestamp>
<date>2005-07-10</date>
<time>15:03:00</time>
<album>
<albumid>1</albumid>
<title>New World Order</title>
<published_year>1990</published_year>
<comment>the best ever!</comment>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
<sale>
<saleid>14</saleid>
<timestamp>2005-07-10 15:09:00</timestamp>
<date>2005-07-10</date>
<time>15:09:00</time>
<album>
<albumid>2</albumid>
<title>Curtis</title>
<published_year>1970</published_year>
<comment>that man's got somthin' to say</comment>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
</sales>
</customer>
<customer>
<customerid>8</customerid>
<first_name>Ed</first_name>
<last_name>Burton</last_name>
<email>ed.burton@example.com</email>
<sales>
<sale>
<saleid>8</saleid>
<timestamp>2005-07-10 16:03:00</timestamp>
<date>2005-07-10</date>
<time>16:03:00</time>
<album>
<albumid>1</albumid>
<title>New World Order</title>
<published_year>1990</published_year>
<comment>the best ever!</comment>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
<sale>
<saleid>19</saleid>
<timestamp>2005-07-10 16:03:01</timestamp>
<date>2005-07-10</date>
<time>16:03:01</time>
<album>
<albumid>3</albumid>
<title>Shaft</title>
<published_year>1972</published_year>
<comment>he's the man</comment>
<artist>
<artistid>2</artistid>
<name>Isaac Hayes</name>
<birth_year>1942</birth_year>
<birth_place>Tennessee</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
</sales>
</customer>
<customer>
<customerid>9</customerid>
<first_name>Jack</first_name>
<last_name>Woo</last_name>
<email>jack.woo@example.com</email>
<sales>
<sale>
<saleid>9</saleid>
<timestamp>2005-07-10 18:03:00</timestamp>
<date>2005-07-10</date>
<time>18:03:00</time>
<album>
<albumid>1</albumid>
<title>New World Order</title>
<published_year>1990</published_year>
<comment>the best ever!</comment>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
<sale>
<saleid>15</saleid>
<timestamp>2005-07-10 18:49:00</timestamp>
<date>2005-07-10</date>
<time>18:49:00</time>
<album>
<albumid>2</albumid>
<title>Curtis</title>
<published_year>1970</published_year>
<comment>that man's got somthin' to say</comment>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
</sales>
</customer>
<customer>
<customerid>10</customerid>
<first_name>Maria</first_name>
<last_name>Gonzales</last_name>
<email>maria.gonzales@example.com</email>
<sales>
<sale>
<saleid>10</saleid>
<timestamp>2005-07-10 19:03:00</timestamp>
<date>2005-07-10</date>
<time>19:03:00</time>
<album>
<albumid>1</albumid>
<title>New World Order</title>
<published_year>1990</published_year>
<comment>the best ever!</comment>
<artist>
<artistid>1</artistid>
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
<sale>
<saleid>20</saleid>
<timestamp>2005-07-10 19:03:50</timestamp>
<date>2005-07-10</date>
<time>19:03:50</time>
<album>
<albumid>3</albumid>
<title>Shaft</title>
<published_year>1972</published_year>
<comment>he's the man</comment>
<artist>
<artistid>2</artistid>
<name>Isaac Hayes</name>
<birth_year>1942</birth_year>
<birth_place>Tennessee</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
</sales>
</customer>
</music_store>
case05_debug.phpcase05_debug.php:
<?php
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
require_once 'Log.php';
$debugLogger = Log::factory('file', 'case05.log', 'Query2XML');
$query2xml->enableDebugLog($debugLogger);
$query2xml->startProfiling();
$dom = $query2xml->getXML(
"SELECT
*
FROM
customer c
LEFT JOIN sale s ON c.customerid = s.customer_id
LEFT JOIN album al ON s.album_id = al.albumid
LEFT JOIN artist ar ON al.artist_id = ar.artistid",
array(
'rootTag' => 'music_store',
'rowTag' => 'customer',
'idColumn' => 'customerid',
'elements' => array(
'customerid',
'first_name',
'last_name',
'email',
'sales' => array(
'rootTag' => 'sales',
'rowTag' => 'sale',
'idColumn' => 'saleid',
'elements' => array(
'saleid',
'timestamp',
'date' => '#Callbacks::getFirstWord()',
'time' => '#Callbacks::getSecondWord()',
'album' => array(
'rootTag' => '',
'rowTag' => 'album',
'idColumn' => 'albumid',
'elements' => array(
'albumid',
'title',
'published_year',
'comment',
'artist' => array(
'rootTag' => '',
'rowTag' => 'artist',
'idColumn' => 'artistid',
'elements' => array(
'artistid',
'name',
'birth_year',
'birth_place',
'genre'
) //artist elements
) //artist array
) //album elements
) //album array
) //sales elements
) //sales array
) //root elements
) //root
); //getXML method call
$root = $dom->firstChild;
$root->setAttribute ('date_generated', date("Y-m-d\TH:i:s", 1124801570 ));
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
require_once 'File.php';
$fp = new File();
$fp->write('case05.profile', $query2xml->getProfile(), FILE_MODE_WRITE);
class Callbacks
{
function getFirstWord($record)
{
return substr($record['timestamp'], 0 , strpos($record['timestamp'], ' '));
}
function getSecondWord($record)
{
return substr($record['timestamp'], strpos($record['timestamp'], ' ') + 1 );
}
}
?>
The lines 6-8 do the debugging, line 10 and 76-78 the profiling. This will create
case05.log and case05.profile.
case05.logThe format of a debug log file is documented at Logging and Debugging XML_Query2XML.
Our debug log now contains a single query:
Feb 11 17:27:19 Query2XML [info] QUERY: SELECT
*
FROM
customer c
LEFT JOIN sale s ON c.customerid = s.customer_id
LEFT JOIN album al ON s.album_id = al.albumid
LEFT JOIN artist ar ON al.artist_id = ar.artistid
Feb 11 17:27:19 Query2XML [info] DONE
case05.profileThe format of the output is documented under
XML_Query2XML::getProfile():
FROM_DB FROM_CACHE CACHED AVG_DURATION DURATION_SUM SQL
1 0 false 0.0074028968 0.0074028968 SELECT
*
FROM
customer c
LEFT JOIN sale s ON c.customerid = s.customer_id
LEFT JOIN album al ON s.album_id = al.albumid
LEFT JOIN artist ar ON al.artist_id = ar.artistid
TOTAL_DURATION: 0.22688508033752
DB_DURATION: 0.050441980361938
Case 06: BIG join over 10 tablesCase 06 will teach you:
Case 06 demonstrates how complex things can get :) First have a look at the
ER diagram.
It shows a company that sells records. The basic structure of the generated
XML document is as follows:
- for each store we want a list of all departments that are located in this store
- for each department we want a list of all employees that work in this department
- for each employee we want a list of all his sales
- for each sale we want to know the customer and the album sold
- for each album we want to know the artist that performed the music
case06.phpcase06.php looks like this:
<?php
require_once 'XML/Query2XML.php';
require_once 'XML/Query2XML/Callback.php';
require_once 'MDB2.php';
/**Static class that provides validation and parsing methods for
* generating XML.
*
* It is static so that we can easyly call its methods from inside
* Query2XML using eval'd code.
*/
class Helper
{
/**Associative array of US postal state codes*/
public static $statePostalCodes = array(
'ALABAMA' => 'AL', 'ALASKA' => 'AK', 'AMERICAN SAMOA' => 'AS', 'ARIZONA' => 'AZ', 'ARKANSAS' => 'AR', 'CALIFORNIA' => 'CA',
'COLORADO' => 'CO', 'CONNECTICUT' => 'CT', 'DELAWARE' => 'DE', 'DISTRICT OF COLUMBIA' => 'DC', 'FEDERATED STATES OF MICRONESIA' => 'FM',
'FLORIDA' => 'FL', 'GEORGIA' => 'GA', 'GUAM' => 'GU', 'HAWAII' => 'HI', 'IDAHO' => 'ID', 'ILLINOIS' => 'IL', 'INDIANA' => 'IN',
'IOWA' => 'IA', 'KANSAS' => 'KS', 'KENTUCKY' => 'KY', 'LOUISIANA' => 'LA', 'MAINE' => 'ME', 'MARSHALL ISLANDS' => 'MH', 'MARYLAND' => 'MD',
'MASSACHUSETTS' => 'MA', 'MICHIGAN' => 'MI', 'MINNESOTA' => 'MN', 'MISSISSIPPI' => 'MS', 'MISSOURI' => 'MO', 'MONTANA' => 'MT',
'NEBRASKA' => 'NE', 'NEVADA' => 'NV', 'NEW HAMPSHIRE' => 'NH', 'NEW JERSEY' => 'NJ', 'NEW JESEY' => 'NJ', 'NEW MEXICO' => 'NM', 'NEW YORK' => 'NY',
'NORTH CAROLINA' => 'NC', 'NORTH DAKOTA' => 'ND', 'NORTHERN MARIANA ISLANDS' => 'MP', 'OHIO' => 'OH', 'OKLAHOMA' => 'OK', 'OREGON' => 'OR',
'PALAU' => 'PW', 'PENNSYLVANIA' => 'PA', 'PUERTO RICO' => 'PR', 'RHODE ISLAND' => 'RI', 'SOUTH CAROLINA' => 'SC', 'SOUTH DAKOTA' => 'SD',
'TENNESSEE' => 'TN', 'TEXAS' => 'TX', 'UTAH' => 'UT', 'VERMONT' => 'VT', 'VIRGIN ISLANDS' => 'VI', 'VIRGINIA' => 'VA', 'WASHINGTON' => 'WA',
'WEST VIRGINIA' => 'WV', 'WISCONSIN' => 'WI', 'WYOMING' => 'WY'
);
/**Translates a US state name into its two-letter postal code.
* If the translation fails, $state is returned unchanged
* @param $record The record
*/
public static function getStatePostalCode($record)
{
$state = $record["state"];
if (isset(self::$statePostalCodes[$s])) {
return self::$statePostalCodes[$s];
} else {
return $state;
}}
function summarize($str, $limit=50, $appendString=' ...')
{
if (strlen($str) > $limit) {
$str = substr($str, 0 , $limit - strlen($appendString)) . $appendString;
}
return $str;
}
function summarizeComment($record, $limit)
{
return self::summarize($record["comment"], $limit);
}
}
/**Command class that implements the command pattern.
* It implements the XML_Query2XML_Callback interface
* and therefore has to provide the public non-static
* method execute(array $record).
*/
class UppercaseColumnCommand implements XML_Query2XML_Callback
{
public function __construct($columnName)
{
$this->_columnName = $columnName;
}
public function execute(array $record)
{
return strtoupper($record[$this->_columnName]);
}
}
$dom = $query2xml->getXML(
"SELECT
s.*,
manager.employeeid AS manager_employeeid,
manager.employeename AS manager_employeename,
d.*,
department_head.employeeid AS department_head_employeeid,
department_head.employeename AS department_head_employeename,
e.*,
sa.*,
c.*,
al.*,
ar.*,
(SELECT COUNT(*) FROM sale WHERE sale.store_id = s.storeid) AS store_sales,
(SELECT
COUNT(*)
FROM
sale, employee, employee_department
WHERE
sale.employee_id = employee.employeeid
AND
employee_department.employee_id = employee.employeeid
AND
employee_department.department_id = d.departmentid
) AS department_sales,
(SELECT
COUNT(*)
FROM
employee, employee_department, department
WHERE
employee_department.employee_id = employee.employeeid
AND
employee_department.department_id = department.departmentid
AND
department.store_id = s.storeid
) AS store_employees,
(SELECT
COUNT(*)
FROM
employee, employee_department
WHERE
employee_department.employee_id = employee.employeeid
AND
employee_department.department_id = d.departmentid
) AS department_employees
FROM
store s
LEFT JOIN employee manager ON s.manager = manager.employeeid
LEFT JOIN department d ON d.store_id = s.storeid
LEFT JOIN employee department_head ON department_head.employeeid = d.department_head
LEFT JOIN employee_department ed ON ed.department_id = d.departmentid
LEFT JOIN employee e ON e.employeeid = ed.employee_id
LEFT JOIN sale sa ON sa.employee_id = e.employeeid
LEFT JOIN customer c ON c.customerid = sa.customer_id
LEFT JOIN album al ON al.albumid = sa.album_id
LEFT JOIN artist ar ON ar.artistid = al.artist_id",
array(
'rootTag' => 'music_company',
'rowTag' => 'store',
'idColumn' => 'storeid',
'attributes' => array(
'storeid'
),
'elements' => array(
'store_sales',
'store_employees',
'manager' => array(
'idColumn' => 'manager_employeeid',
'attributes' => array(
'manager_employeeid'
),
'elements' => array(
'manager_employeename'
)
),
'address' => array(
'elements' => array(
'country',
'state' => '#Helper::getStatePostalCode()',
'city' => new UppercaseColumnCommand('city'),
'street',
'phone'
)
),
'department' => array(
'idColumn' => 'departmentid',
'attributes' => array(
'departmentid'
),
'elements' => array(
'department_sales',
'department_employees',
'departmentname',
'department_head' => array(
'idColumn' => 'department_head_employeeid',
'attributes' => array(
'department_head_employeeid'
),
'elements' => array(
'department_head_employeename'
)
),
'employees' => array(
'rootTag' => 'employees',
'rowTag' => 'employee',
'idColumn' => 'employeeid',
'attributes' => array(
'employeeid'
),
'elements' => array(
'employeename',
'sales' => array(
'rootTag' => 'sales',
'rowTag' => 'sale',
'idColumn' => 'saleid',
'attributes' => array(
'saleid'
),
'elements' => array(
'timestamp',
'customer' => array(
'idColumn' => 'customerid',
'attributes' => array(
'customerid'
),
'elements' => array(
'first_name',
'last_name',
'email'
)
),
'album' => array(
'idColumn' => 'albumid',
'attributes' => array(
'albumid'
),
'elements' => array(
'title',
'published_year',
'comment' => '?#Helper::summarizeComment(12)',
'artist' => array(
'idColumn' => 'artistid',
'attributes' => array(
'artistid'
),
'elements' => array(
'name',
'birth_year',
'birth_place',
'genre'
)
)
) // album elements
) //album array
) //sales elements
) //sales array
) //employees elements
) //employees array
) //department elements
) // department array
) //root elements
) //root
); //getXML method call
$root = $dom->firstChild;
$root->setAttribute ('date_generated', date("Y-m-d\TH:i:s", 1124801570 ));
header('Content-Type: application/xml');
$dom->formatOutput = true;
print $dom->saveXML();
?>
Note how $options['idColumn'] is used at the different
levels:
- on the root level we want a record for each store and therefore use the primary key of the table 'store' as
the idColumn:
'idColumn' => 'storeid',
- [elements][address] does not use $options['idColumn']
at all because all columns used at this level ('country',
'state', 'city', 'street' and 'phone') are all columns of the table 'store'.
- on the level [elements][department] we want a record for each department (within the current store)
and therefore use the primary key of the table 'department' as the idColumn:
'idColumn' => 'departmentid',
The reason we can use department.departmentid without listing all departments underneath all stores is of course that
our LEFT JOIN has the condition "LEFT JOIN department ON department.store_id = store.storeid".
- on the level [elements][department][elements][employees] we want a record for each employee (within
the current department) and therefore use the primary key of the table 'employee' as the idColumn:
'idColumn' => 'employeeid',
The reason we can use employee.employeeid without listing all employees underneath all departments is of course that
our LEFT JOIN does the trick for us (implementing a n:n relationship via the table employee_department).
- on the level [elements][department][elements][employees][elements][sales] we want a record for each sale
(perfmormed by the current employee) and therefore use the primary key of the table 'sale' as the idColumn:
'idColumn' => 'saleid',
The reason we can use sale.saleid without listing all sales underneath all employees is of course that
our LEFT JOIN has the condition "LEFT JOIN sale ON sale.employee_id = employee.employeeid".
- on the level [elements][department][elements][employees][elements][sales][elements][customer] we want a
record for each customer (which was a party to the current sale) and therefore use the primary key of
the table 'customer' as the idColumn:
'idColumn' => 'customerid',
Logically speaking we are of course using the foreign key sale.employee_id, but as the equality of
sale.employee_id and employee.employeeid is the condition for our LEFT JOIN, both are the same.
- on the level [elements][department][elements][employees][elements][sales][elements][album] we want a
record for each album (which was subject to the current sale) and therefore use the primary key of
the table 'album' as the idColumn:
'idColumn' => 'albumid',
Logically speaking we are of course using the foreign key sale.album_id, but as the equality of
sale.album_id and album.albumid is the condition for our LEFT JOIN, both are the same.
- on the level [elements][department][elements][employees][elements][sales][elements][album][elements][artist]
we want a record for each artist (who permormed the current album) and therefore use the primary key of
the table 'artist' as the idColumn:
'idColumn' => 'artistid',
Logically speaking we are of course using the foreign key album.artist_id, but as the equality of
album.artist_id and artist.artistid is the condition for our LEFT JOIN, both are the same.
We also use a static class called Helper here. Note how Helper::summarizeComment() is called with a second argument.
The current record is always passed as first argument to the callback function. So we specify the "comment"
element as
'comment' => '?#Helper::summarizeComment(12)',
which means that the string '12' will be passed as second argument to Helper::summarizeComment().
The CONDITIONAL prefix ? means that the comment element will only appear if the (summarized)
comment != "".
In addition to the CALLBACK FUNCTION we also use a COMMAND OBJECT. In our case it is
an instance of the class UppercaseColumnCommand which implements the XML_Query2XML_Callback
interface. We pass the column's name as constructor argument, so that UppercaseColumnCommand::execute()
knows which column (i.e. which element of the $record array) to pass to strtoupper().
case06.xmlThe resulting XML data looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<music_company date_generated="2005-08-23T14:52:50">
<store storeid="1">
<store_sales>10</store_sales>
<store_employees>6</store_employees>
<manager manager_employeeid="1">
<manager_employeename>Michael Jones</manager_employeename>
</manager>
<address>
<country>US</country>
<state>NY</state>
<city>NEW YORK</city>
<street>Broadway & 72nd Str</street>
<phone>123 456 7890</phone>
</address>
<department departmentid="1">
<department_sales>10</department_sales>
<department_employees>3</department_employees>
<departmentname>Sales</departmentname>
<department_head department_head_employeeid="1">
<department_head_employeename>Michael Jones</department_head_employeename>
</department_head>
<employees>
<employee employeeid="1">
<employeename>Michael Jones</employeename>
<sales>
<sale saleid="1">
<timestamp>2005-05-25 16:32:00</timestamp>
<customer customerid="1">
<first_name>Jane</first_name>
<last_name>Doe</last_name>
<email>jane.doe@example.com</email>
</customer>
<album albumid="1">
<title>New World Order</title>
<published_year>1990</published_year>
<comment>the best ...</comment>
<artist artistid="1">
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
<sale saleid="7">
<timestamp>2005-07-10 15:03:00</timestamp>
<customer customerid="7">
<first_name>Nick</first_name>
<last_name>Fallow</last_name>
<email>nick.fallow@example.com</email>
</customer>
<album albumid="1">
<title>New World Order</title>
<published_year>1990</published_year>
<comment>the best ...</comment>
<artist artistid="1">
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
<sale saleid="16">
<timestamp>2005-06-05 12:56:12</timestamp>
<customer customerid="2">
<first_name>John</first_name>
<last_name>Doe</last_name>
<email>john.doe@example.com</email>
</customer>
<album albumid="3">
<title>Shaft</title>
<published_year>1972</published_year>
<comment>he's the man</comment>
<artist artistid="2">
<name>Isaac Hayes</name>
<birth_year>1942</birth_year>
<birth_place>Tennessee</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
<sale saleid="19">
<timestamp>2005-07-10 16:03:01</timestamp>
<customer customerid="8">
<first_name>Ed</first_name>
<last_name>Burton</last_name>
<email>ed.burton@example.com</email>
</customer>
<album albumid="3">
<title>Shaft</title>
<published_year>1972</published_year>
<comment>he's the man</comment>
<artist artistid="2">
<name>Isaac Hayes</name>
<birth_year>1942</birth_year>
<birth_place>Tennessee</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
</sales>
</employee>
<employee employeeid="2">
<employeename>Susi Weintraub</employeename>
<sales>
<sale saleid="3">
<timestamp>2005-07-10 11:03:00</timestamp>
<customer customerid="3">
<first_name>Susan</first_name>
<last_name>Green</last_name>
<email>susan.green@example.com</email>
</customer>
<album albumid="1">
<title>New World Order</title>
<published_year>1990</published_year>
<comment>the best ...</comment>
<artist artistid="1">
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
<sale saleid="9">
<timestamp>2005-07-10 18:03:00</timestamp>
<customer customerid="9">
<first_name>Jack</first_name>
<last_name>Woo</last_name>
<email>jack.woo@example.com</email>
</customer>
<album albumid="1">
<title>New World Order</title>
<published_year>1990</published_year>
<comment>the best ...</comment>
<artist artistid="1">
<name>Curtis Mayfield</name>
<birth_year>1920</birth_year>
<birth_place>Chicago</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
<sale saleid="17">
<timestamp>2005-07-10 10:03:32</timestamp>
<customer customerid="4">
<first_name>Victoria</first_name>
<last_name>Alt</last_name>
<email>victory.alt@example.com</email>
</customer>
<album albumid="3">
<title>Shaft</title>
<published_year>1972</published_year>
<comment>he's the man</comment>
<artist artistid="2">
<name>Isaac Hayes</name>
<birth_year>1942</birth_year>
<birth_place>Tennessee</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
<sale saleid="20">
<timestamp>2005-07-10 19:03:50</timestamp>
<customer customerid="10">
<first_name>Maria</first_name>
<last_name>Gonzales</last_name>
<email>maria.gonzales@example.com</email>
</customer>
<album albumid="3">
<title>Shaft</title>
<published_year>1972</published_year>
<comment>he's the man</comment>
<artist artistid="2">
<name>Isaac Hayes</name>
<birth_year>1942</birth_year>
<birth_place>Tennessee</birth_place>
<genre>Soul</genre>
</artist>
</album>
</sale>
</sales>
</employee>
<employee employeeid="3">
<employeename>Steve Hack</employeename>
<sales>
<sale saleid="5">
<timestamp>2005-07-10 13:03:00</timestamp>
<customer customerid="5">
<first_name>Will</first_name>
<last_name>Rippy</last_name>
<email>will.wippy@example.com</email>
</customer>
<album albumid=&
|
|