XML_Query2XML::getXML()
XML_Query2XML::getXML($sql, $options)
This method is the most powerful transformation method. It returns an instance of
DOMDocument (part of PHP5's built-in DOM API). The records returned by the query/queries will be processed
one after another. The $options argument is a rather complex, associative,
multi dimensional array. The $sql argument can be a string or as well an associative array.
$sql
This option is almost exactly like $options['sql']: you
can specify the query with a Simple Query Specification
or a Complex Query Specification.
What is different from $options['sql'] is that
you can also specify a boolean value of false.
Here is an example of a simple query specification (WARNING: to prevent SQL injection
vulerabilities you should use a complex query specification when dealing with non-static queries like this one):
<?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 Specifications
These allow you to use an array to specify elements that have only two properties: a name and a value.
The array values are used to specify the XML element values whereas the array keys are used to specify the
XML element names. For all array elements that are defined without a key, the array values will be used for
the XML element names. If no prefix is used (see below) the contents of the array values are interpreted
as column names. The following example illustrates the most basic usage of a simple element specification:
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 Specifications
A complex element specification consists of an array that can have all options
that can be present on the root level plus $options['sql']
and $options['sql_options'].
This allows for complete (and theoretically infinite) nesting. You will need to use it if the
child element should have attributes or child elements.
The following example is like the first one in
Simple Element Specifications
with one difference: the XML element 'name' should have the attribute 'type' set to the
static value 'full_name'. As attributes are not supported by simple elements specifications,
we have to use a complex element specification for the element 'name':
<?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 Shortcut
The asterisk shortcut only works with Simple Element Specifications
(and Simple Attribute Specifications).
In some scenarios you will just want to use all columns found in the result set
for Simple Element Specifications.
This is where the asterisk shortcut can come in very handy. An element specification that
contains an asterisk (an "asterisk element specification") will be duplicated for each
column present in the result set ($record). The simplest way of using the asterisk shortcut is this:
<?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 Keys
Sometimes your primary key will consist of multiple columns. For example, this might
be the case when you implement a many-to-many relationship using an intersection table.
But as you know by now,
$options['idColumn'] has to evaluate to one unique ID for every record.
Depending on the type of the primary key columns you will want to choose a different strategy to
compute that unique ID for every record. To begin with, you have to choose whether
you want to compute that unique ID within the database or using PHP. To do it within the
database you will have to define an alias using the "... AS alias_name" syntax. Using
PHP you have to use a callback function to generate the ID. When generating the ID, you
again have different options.
If your primary key columns are of a numeric type, you can
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 Specifications
It works like
Simple Element Specifications:
the column names are the array values. By default the column's value
will be put into an attribute named after the column. If you're
unhappy with the default you can specify an other attribute name by using
it as the array key. As documented for Simple Element Specifications
the prefixes "?", "#", "^" and ":" or a COMMAND OBJECT can be used. Only the UNSERIALIZATION prefix & and the CDATA SECTION prefix ^ which are valid for
a Simple Element Specification
cannot be used for a Simple Attribute Specification.
The follwing example will use
the column name as the attribute name for the first two columns but the
custom attribute name 'music_genre' for the column 'genre':
<?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 Specifications
A complex attribute specification consists of an array that must contain
- $options['value']: the attribute's value
(note: you cannot use the UNSERIALIZATION prefix & or the the CDATA SECTION prefix ^ for an attribute specification)
and optionally can contain
The array key used to store the complex attribute specification is always used as the attribute's name.
Unlike Complex Element Specifications
complex attribute specifications cannot be nested for obvious reasons. Complex attribute specifications
should only be used for the following reasons:
- the attribute is only to be included under a condition that cannot be expressed using the '?' prefix
within a simple attribute specification
- additional data is needed from the database
In all other cases Simple Attribute Specifications
should be used as they will make your code run faster.
To add a "bornBefore1940" attribute only to those artists that were born before 1940 we could write:
<?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 Shortcut
The asterisk shortcut only works with Simple Attribute Specifications
(and Simple Element Specifications).
Everything said about
Using the Asterisk Shortcut with simple element specifications
applies here to!
The simplest example of using the asterisk shortcut with the attributes option is as follows:
<?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 Specification
Since v0.8.0 a simple query specifications are purely static strings (in most cases
you will want to use a Complex Query Specification):
<?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 Specification
A Complex Query Specification uses the database abstraction layer's prepare() and execute() methods
and therefore prevents SQL injection and is also faster
than a Simple Query Specification in most scenarios.
It can consist of multiple parts (only $options['sql']['query'] is mandatory):
- $options['sql']['query']: the SQL query as a string that contains a placeholder
for each element of $options['sql']['data'].
- $options['sql']['driver']: allows you to use a different XML_Query2XML_Driver
for this complex query than the one passed to
XML_Query2XML::factory(). Please see
Using Multiple Drivers for details. $options['sql']['driver'] is optional.
- $options['sql']['limit']: allows you to limit the number of records returned from the
query. It has to be a numeric value. Please note that a value of 0 (or '0') is equivalent to not
setting $options['sql']['limit'] at all. $options['sql']['limit'] and $options['sql']['offset'] are
only interpreted by the drivers for PEAR MDB2 and PEAR DB. All other drivers simply ignore these two
options.
- $options['sql']['offset']: allows you to set the number of the first record
to retrieve. This has to be a numeric value. The default is 0. Please note that this option will
be ignored unless $options['sql']['limit'] is set. $options['sql']['offset'] and
$options['sql']['limit'] are only interpreted by the drivers for PEAR MDB2 and PEAR DB.
All other drivers simply ignore these two options.
- $options['sql']['data']: an indexed array of values. This is optional.
The specification can be of the following types:
- COLUMN NAME: this is the default if not preceeded by ':' or '#'.
If the column does not exist, an XML_Query2XML_ConfigException will be thrown. Note
that the parent record will be used! This is quite logic as this SQL statement has not been executed yet :)
- STATIC TEXT with a : prefix: if the value is preceeded by a colon (':'), it is
interpreted as static text.
- CALLBACK FUNCTION with a # prefix: if the value is preceeded by a pound sign ('#'), it
is interpreted as a callback function. You can use a regular function (e.g. '#myFunction()') or a static
method (e.g. '#MyClass::myFunction()') - for how to use a non-static method, see the type COMMAND OBJECT.
The current record will be passed to the callback function
as an associative array. You can also pass additional string arguments to the callback function by specifing
them within the opening and closing brace; e.g. '#Utils::limit(12)' will result in Util::limit() being
called with the current record as the first and '12' as the second argument. If you do not want
to pass additional arguments to the callback function, the opening and closing brace are optional.
- COMMAND OBJECT (object): If you want to use a non-static method as a
callback function, you can do so by specifying the value as an instance of a class that
implements the XML_Query2XML_Callback interface. This implementation of the
command pattern gives you all the
flexibility. Note: you have to require_once 'XML/Query2XML/Callback.php'
before using the XML_Query2XML_Callback interface. The return value of a COMMAND OBJECT's
execute() method is treated exactly the same as the return value of a CALLBACK FUNCTION.
The same syntax (with the additional '?' prefix) can be use for
Simple Element Specifications,
$options['value'],
Simple Attribute Specifications and
$options['idColumn'] because the private method
XML_Query2XML::_applyColumnStringToRecord() is used in all cases.
Note: $options['sql']['data'] is optional!
Here is a simple example similar to Case 03: Two SELECTs instead of a LEFT OUTER JOIN:
<?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:
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:2005
The package XML_Query2XML also implements the Final Committee Draft for ISO/IEC 9075-14:2005,
section "9.1 Mapping SQL <identifier>s to XML Names". ISO/IEC 9075-14:2005 is available
online at http://www.sqlx.org/SQL-XML-documents/5FCD-14-XML-2004-07.pdf.
A lot of characters are legal in SQL identifiers but cannot be used within
XML names. To begin with, SQL identifiers can contain any Unicode character
while XML names are limited to a certain set of characters. E.g the
SQL identifier "<21yrs in age" obviously is not a valid XML name.
'#', '{', and '}' are also not allowed. Fully escaped SQL identifiers
also must not contain a column (':') or start with "xml" (in any case
combination). Illegal characters are mapped to a string of the form
_xUUUU_ where UUUU is the Unicode value of the character.
The following is a table of example mappings:
+----------------+------------------------+------------------------------------+
| SQL-Identifier | Fully escaped XML name | Comment |
+----------------+------------------------+------------------------------------+
| dept:id | dept_x003A_id | ":" is illegal |
| xml_name | _x0078_ml_name | must not start with [Xx][Mm][Ll] |
| XML_name | _x0058_ML_name | must not start with [Xx][Mm][Ll] |
| hire date | hire_x0020_date | space is illegal too |
| Works@home | Works_x0040_home | "@" is illegal |
| file_xls | file_x005F_xls | "_" gets mapped if followed by "x" |
| FIRST_NAME | FIRST_NAME | no problem here |
+----------------+------------------------+------------------------------------+
The ISO 9075-mapping does produce some overhead which might not be needed in
a lot of situations. Therefore it is not the default mapper. In most cases
it will be sufficient to validate your XML schema once using tools like the free
XMLSpy Home Edition.
To use the ISO 9075-mapper that comes with XML_Query2XML you have to:
Here goes an example:
<?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 mappers
There are cases when you will want the tag and attribute names to be somehow different from
the column names. Let's say you want to use the column names as tag and attribute names
but make them all uppercase. Certainly you could write code like this:
<?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 mappers
Let's say we want to force all tags corresponding to columns of the artist table to be uppercase
and all tags corresponding to columns of the album table to be lowercase. This can be done
using two mappers:
<?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:
- 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().
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:
UTF-8 to ISO-8859-1 encoding
If your data is in UTF-8 but you would like your XML to be in ISO-8859-1 (Latin-1),
you can use utf8_decode():
'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 encoding
If you data already is in the character set you wish to use for the XML, all you
have to do is to disable the encoding by using a boolean value of false:
Using multiple encoders
It might happen to you that some of your data sources are in one character set
while others are in another. This means that you need different encoding procedures
to convert them all to the same character set you wish to use for the XML (usually UTF-8).
In the first example we will assume that all columns of the table artist are in
ISO-8859-1 (Latin-1) while all columns of the table album are in UTF-8.
<?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 instance
XML_Query2XML::getXML() returns an instance of
DOMDocument.
I recommend that you do some reading about
PHP5's DOM extension.
Let's see how we can add attributes to the root element
(i.e. the first child of the DOMDocument instance returned by getXML()):
<?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>
<