Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
/**
3
 * This file contains the class XML_Query2XML and all exception classes.
4
 *
5
 * PHP version 5
6
 *
7
 * @category  XML
8
 * @package   XML_Query2XML
9
 * @author    Lukas Feiler <lukas.feiler@lukasfeiler.com>
10
 * @copyright 2006 Lukas Feiler
11
 * @license   http://www.gnu.org/copyleft/lesser.html  LGPL Version 2.1
12
 * @version   CVS: $Id: Query2XML.php 309897 2011-04-02 17:36:42Z lukasfeiler $
13
 * @link      http://pear.php.net/package/XML_Query2XML
14
 */
15
 
16
/**
17
 * PEAR_Exception is used as the parent for XML_Query2XML_Exception.
18
 */
19
require_once 'PEAR/Exception.php';
20
 
21
/**
22
 * Create XML data from SQL queries.
23
 *
24
 * XML_Query2XML heavily uses exceptions and therefore requires PHP5.
25
 * Additionally one of the following database abstraction layers is
26
 * required: PDO (compiled-in by default since PHP 5.1), PEAR DB,
27
 * PEAR MDB2, ADOdb.
28
 *
29
 * The two most important public methods this class provides are:
30
 *
31
 * <b>{@link XML_Query2XML::getFlatXML()}</b>
32
 * Transforms your SQL query into flat XML data.
33
 *
34
 * <b>{@link XML_Query2XML::getXML()}</b>
35
 * Very powerful and flexible method that can produce whatever XML data you want. It
36
 * was specifically written to also handle LEFT JOINS.
37
 *
38
 * They both return an instance of the class DOMDocument provided by PHP5's
39
 * built-in DOM API.
40
 *
41
 * A typical usage of XML_Query2XML looks like this:
42
 * <code>
43
 * <?php
44
 * require_once 'XML/Query2XML.php';
45
 * $query2xml = XML_Query2XML::factory(MDB2::factory($dsn));
46
 * $dom = $query2xml->getXML($sql, $options);
47
 * header('Content-Type: application/xml');
48
 * print $dom->saveXML();
49
 * ?>
50
 * </code>
51
 *
52
 * Please read the <b>{@tutorial XML_Query2XML.pkg tutorial}</b> for
53
 * detailed usage examples and more documentation.
54
 *
55
 * @category  XML
56
 * @package   XML_Query2XML
57
 * @author    Lukas Feiler <lukas.feiler@lukasfeiler.com>
58
 * @copyright 2006 Lukas Feiler
59
 * @license   http://www.gnu.org/copyleft/lesser.html  LGPL Version 2.1
60
 * @version   Release: 1.7.2
61
 * @link      http://pear.php.net/package/XML_Query2XML
62
 */
63
class XML_Query2XML
64
{
65
    /**
66
     * Primary driver.
67
     * @var XML_Query2XML_Driver A subclass of XML_Query2XML_Driver.
68
     */
69
    private $_driver;
70
 
71
    /**
72
     * An instance of PEAR Log
73
     * @var mixed An object that has a method with the signature log(String $msg);
74
     *            preferably PEAR Log.
75
     * @see enableDebugLog
76
     * @see disableDebugLog
77
     */
78
    private $_debugLogger;
79
 
80
    /**
81
     * Whether debug logging is to be performed
82
     * @var boolean
83
     * @see enableDebugLog
84
     * @see disableDebugLog
85
     */
86
    private $_debug = false;
87
 
88
    /**
89
     * Whether profiling is to be performed
90
     * @var boolean
91
     * @see startProfiling()
92
     * @see stopProfiling()
93
     */
94
    private $_profiling = false;
95
 
96
    /**
97
     * The profiling data.
98
     * @var array A multi dimensional associative array
99
     * @see startProfiling()
100
     * @see stopProfiling()
101
     * @see _debugStartQuery()
102
     * @see _debugStopQuery()
103
     * @see _stopDBProfiling()
104
     */
105
    private $_profile = array();
106
 
107
    /**
108
     * An associative array of global options.
109
     * @var array An associative array
110
     * @see setGlobalOption()
111
     */
112
    private $_globalOptions = array(
113
        'hidden_container_prefix' => '__'
114
    );
115
 
116
    /**
117
     * An associative array that will contain an element for each prefix.
118
     * The prefix is used as the element key. Each array element consists
119
     * of an indexed array containing a file path and a class name.
120
     * @var array An associative multidimensional array.
121
     * @see registerPrefix()
122
     * @see unregisterPrefix()
123
     * @see unregisterAllPrefixes()
124
     * @see _buildCommandChain()
125
     */
126
    private $_prefixes = array(
127
        '?' => array(
128
            'XML/Query2XML/Data/Condition/NonEmpty.php',
129
            'XML_Query2XML_Data_Condition_NonEmpty'
130
        ),
131
        '&' => array(
132
            'XML/Query2XML/Data/Processor/Unserialize.php',
133
            'XML_Query2XML_Data_Processor_Unserialize'
134
        ),
135
        '=' => array(
136
            'XML/Query2XML/Data/Processor/CDATA.php',
137
            'XML_Query2XML_Data_Processor_CDATA'
138
        ),
139
        '^' => array(
140
            'XML/Query2XML/Data/Processor/Base64.php',
141
            'XML_Query2XML_Data_Processor_Base64'
142
        ),
143
        ':' => array(
144
            'XML/Query2XML/Data/Source/Static.php',
145
            'XML_Query2XML_Data_Source_Static'
146
        ),
147
        '#' => array(
148
            'XML/Query2XML/Data/Source/PHPCallback.php',
149
            'XML_Query2XML_Data_Source_PHPCallback'
150
        ),
151
        '~' => array(
152
            'XML/Query2XML/Data/Source/XPath.php',
153
            'XML_Query2XML_Data_Source_XPath'
154
        )
155
    );
156
 
157
    /**
158
     * Constructor
159
     *
160
     * @param mixed $backend A subclass of XML_Query2XML_Driver or
161
     *                       an instance of PEAR DB, PEAR MDB2, ADOdb,
162
     *                       PDO, Net_LDAP2 or Net_LDAP.
163
     */
164
    private function __construct($backend)
165
    {
166
        if ($backend instanceof XML_Query2XML_Driver) {
167
            $this->_driver = $backend;
168
        } else {
169
            $this->_driver = XML_Query2XML_Driver::factory($backend);
170
        }
171
    }
172
 
173
    /**
174
     * Factory method.
175
     * As first argument pass an instance of PDO, PEAR DB, PEAR MDB2, ADOdb,
176
     * Net_LDAP or an instance of any class that extends XML_Query2XML_Driver:
177
     * <code>
178
     * <?php
179
     * require_once 'XML/Query2XML.php';
180
     * $query2xml = XML_Query2XML::factory(
181
     *   new PDO('mysql://root@localhost/Query2XML_Tests')
182
     * );
183
     * ?>
184
     * </code>
185
     *
186
     * <code>
187
     * <?php
188
     * require_once 'XML/Query2XML.php';
189
     * require_once 'DB.php';
190
     * $query2xml = XML_Query2XML::factory(
191
     *   DB::connect('mysql://root@localhost/Query2XML_Tests')
192
     * );
193
     * ?>
194
     * </code>
195
     *
196
     * <code>
197
     * <?php
198
     * require_once 'XML/Query2XML.php';
199
     * require_once 'MDB2.php';
200
     * $query2xml = XML_Query2XML::factory(
201
     *   MDB2::factory('mysql://root@localhost/Query2XML_Tests')
202
     * );
203
     * ?>
204
     * </code>
205
     *
206
     * <code>
207
     * <?php
208
     * require_once 'XML/Query2XML.php';
209
     * require_once 'adodb/adodb.inc.php';
210
     * $adodb = ADONewConnection('mysql');
211
     * $adodb->Connect('localhost', 'root', '', 'Query2XML_Tests');
212
     * $query2xml = XML_Query2XML::factory($adodb);
213
     * ?>
214
     * </code>
215
     *
216
     * @param mixed $backend An instance of PEAR DB, PEAR MDB2, ADOdb, PDO,
217
     *                       Net_LDAP or a subclass of XML_Query2XML_Driver.
218
     *
219
     * @return XML_Query2XML A new instance of XML_Query2XML
220
     * @throws XML_Query2XML_DriverException If $backend already is a PEAR_Error.
221
     * @throws XML_Query2XML_ConfigException If $backend is not an instance of a
222
     *                  child class of DB_common, MDB2_Driver_Common, ADOConnection
223
     *                  PDO, Net_LDAP or XML_Query2XML_Driver.
224
     */
225
    public static function factory($backend)
226
    {
227
        return new XML_Query2XML($backend);
228
    }
229
 
230
    /**
231
     * Register a prefix that can be used in all value specifications.
232
     *
233
     * @param string $prefix    The prefix name. This must be a single chracter.
234
     * @param string $className The name of the Data Class. This class has
235
     *                          to extend XML_Query2XML_Data.
236
     * @param string $filePath  The path to the file that contains the Command
237
     *                          class. This argument is optional.
238
     *
239
     * @return void
240
     * @throws XML_Query2XML_ConfigException Thrown if $prefix is not a string
241
     *                                       or has a length other than 1.
242
     */
243
    public function registerPrefix($prefix, $className, $filePath = '')
244
    {
245
        if (!is_string($prefix) || strlen($prefix) != 1) {
246
            throw new XML_Query2XML_ConfigException(
247
                'Prefix name has to be a single character'
248
            );
249
        }
250
        $this->_prefixes[$prefix] = array(
251
            $filePath,
252
            $className
253
        );
254
    }
255
 
256
    /**
257
     * Unregister a prefix.
258
     *
259
     * @param string $prefix The prefix name.
260
     *
261
     * @return void
262
     */
263
    public function unregisterPrefix($prefix)
264
    {
265
        unset($this->_prefixes[$prefix]);
266
    }
267
 
268
    /**
269
     * Unregister all prefixes.
270
     *
271
     * @return void
272
     */
273
    public function unregisterAllPrefixes()
274
    {
275
        $this->_prefixes = array();
276
    }
277
 
278
    /**
279
     * Set a global option.
280
     * Currently the following global options are available:
281
     *
282
     * hidden_container_prefix: The prefix to use for container elements that are
283
     *   to be removed before the DOMDocument before it is returned by
284
     *   {@link XML_Query2XML::getXML()}. This has to be a non-empty string.
285
     *   The default value is '__'.
286
     *
287
     * @param string $option The name of the option
288
     * @param mixed  $value  The option value
289
     *
290
     * @return void
291
     * @throws XML_Query2XML_ConfigException If the configuration option
292
     *         does not exist or if the value is invalid for that option
293
     */
294
    public function setGlobalOption($option, $value)
295
    {
296
        switch ($option) {
297
        case 'hidden_container_prefix':
298
            if (is_string($value) && strlen($value) > 0) {
299
                // unit test: setGlobalOption/hidden_container_prefix.phpt
300
                $this->_globalOptions[$option] = $value;
301
            } else {
302
                /*
303
                 * unit test: setGlobalOption/
304
                 * configException_hidden_container_prefix_wrongTypeObject.phpt
305
                 * configException_hidden_container_prefix_wrongTypeEmptyStr.phpt
306
                 */
307
                throw new XML_Query2XML_ConfigException(
308
                    'The value for the hidden_container_prefix option '
309
                    . 'has to be a non-empty string'
310
                );
311
            }
312
            break;
313
 
314
        default:
315
            // unit tests: setGlobalOption/configException_noSuchOption.phpt
316
            throw new XML_Query2XML_ConfigException(
317
                'No such global option: ' . $option
318
            );
319
        }
320
    }
321
 
322
    /**
323
     * Returns the current value for a global option.
324
     * See {@link XML_Query2XML::setGlobalOption()} for a list
325
     * of available options.
326
     *
327
     * @param string $option The name of the option
328
     *
329
     * @return mixed The option's value
330
     * @throws XML_Query2XML_ConfigException If the option does not exist
331
     */
332
    public function getGlobalOption($option)
333
    {
334
        if (!isset($this->_globalOptions[$option])) {
335
            // unit test: getGlobalOption/configException_noSuchOption.phpt
336
            throw new XML_Query2XML_ConfigException(
337
                'No such global option: ' . $option
338
            );
339
        }
340
        // unit test: getGlobalOption/hidden_container_prefix.phpt
341
        return $this->_globalOptions[$option];
342
    }
343
 
344
    /**
345
     * Enable the logging of debug messages.
346
     * This will include all queries sent to the database.
347
     * Example:
348
     * <code>
349
     * <?php
350
     * require_once 'Log.php';
351
     * require_once 'XML/Query2XML.php';
352
     * $query2xml = XML_Query2XML::factory(MDB2::connect($dsn));
353
     * $debugLogger = Log::factory('file', 'out.log', 'XML_Query2XML');
354
     * $query2xml->enableDebugLog($debugLogger);
355
     * ?>
356
     * </code>
357
     * Please see {@link http://pear.php.net/package/Log} for details on PEAR Log.
358
     *
359
     * @param mixed $log Most likely an instance of PEAR Log but any object
360
     *                   that provides a method named 'log' is accepted.
361
     *
362
     * @return void
363
     */
364
    public function enableDebugLog($log)
365
    {
366
        // unit test: enableDebugLog/enableDebugLog.phpt
367
        $this->_debugLogger = $log;
368
        $this->_debug       = true;
369
    }
370
 
371
    /**
372
     * Disable the logging of debug messages
373
     *
374
     * @return void
375
     */
376
    public function disableDebugLog()
377
    {
378
        // unit test: disableDebugLog/disableDebugLog.phpt
379
        $this->_debug = false;
380
    }
381
 
382
    /**
383
     * Start profiling.
384
     *
385
     * @return void
386
     */
387
    public function startProfiling()
388
    {
389
        // unit tests: startProfile/startProfile.phpt
390
        $this->_profiling = true;
391
        $this->_profile   = array(
392
            'queries'    => array(),
393
            'start'      => microtime(1),
394
            'stop'       => 0,
395
            'duration'   => 0,
396
            'dbStop'     => 0,
397
            'dbDuration' => 0
398
        );
399
    }
400
 
401
    /**
402
     * Stop profiling.
403
     *
404
     * @return void
405
     */
406
    public function stopProfiling()
407
    {
408
        // unit test: stopProfile/stopProfile.phpt
409
        $this->_profiling = false;
410
        if (isset($this->_profile['start']) && $this->_profile['stop'] == 0) {
411
            $this->_profile['stop']     = microtime(1);
412
            $this->_profile['duration'] =
413
                $this->_profile['stop'] - $this->_profile['start'];
414
        }
415
    }
416
 
417
    /**
418
     * Returns all raw profiling data.
419
     * In 99.9% of all cases you will want to use getProfile().
420
     *
421
     * @see getProfile()
422
     * @return array
423
     */
424
    public function getRawProfile()
425
    {
426
        // unit test: getRawProfile/getRawProfile.phpt
427
        $this->stopProfiling();
428
        return $this->_profile;
429
    }
430
 
431
    /**
432
     * Returns the profile as a single multi line string.
433
     *
434
     * @return string The profiling data.
435
     */
436
    public function getProfile()
437
    {
438
        // unit test: getProfile/getProfile.phpt
439
        $this->stopProfiling();
440
        if (count($this->_profile) === 0) {
441
            return '';
442
        }
443
        $ret = 'COUNT AVG_DURATION DURATION_SUM SQL' . "\n";
444
        foreach ($this->_profile['queries'] as $sql => $value) {
445
            $durationSum   = 0.0;
446
            $durationCount = 0;
447
            $runTimes      =& $this->_profile['queries'][$sql]['runTimes'];
448
            foreach ($runTimes as $runTime) {
449
                $durationSum += ($runTime['stop'] - $runTime['start']);
450
                ++$durationCount;
451
            }
452
            if ($durationCount == 0) {
453
                // so that division does not fail
454
                $durationCount = 1;
455
            }
456
            $durationAverage = $durationSum / $durationCount;
457
 
458
            $ret .= str_pad($this->_profile['queries'][$sql]['count'], 5)
459
                  . ' '
460
                  . substr($durationAverage, 0, 12). ' '
461
                  . substr($durationSum, 0, 12). ' '
462
                  . $sql . "\n";
463
        }
464
        $ret .= "\n";
465
        $ret .= 'TOTAL_DURATION: ' . $this->_profile['duration'] . "\n";
466
        $ret .= 'DB_DURATION:    ' . $this->_profile['dbDuration'] . "\n";
467
        return $ret;
468
    }
469
 
470
    /**
471
     * Calls {@link XML_Query2XML::stopProfiling()} and then clears the profiling
472
     * data by resetting a private property.
473
     *
474
     * @return void
475
     */
476
    public function clearProfile()
477
    {
478
        // unit test: clearProfile/clearProfile.phpt
479
        $this->stopProfiling();
480
        $this->_profile = array();
481
    }
482
 
483
    /**
484
     * Transforms the data retrieved by a single SQL query into flat XML data.
485
     *
486
     * This method will return a new instance of DOMDocument. The column names
487
     * will be used as element names.
488
     *
489
     * Example:
490
     * <code>
491
     * <?php
492
     * require_once 'XML/Query2XML.php';
493
     * $query2xml = XML_Query2XML::factory(MDB2::connect($dsn));
494
     * $dom = $query2xml->getFlatXML(
495
     *   'SELECT * FROM artist',
496
     *   'music_library',
497
     *   'artist'
498
     * );
499
     * ?>
500
     * </code>
501
     *
502
     * @param string $sql         The query string.
503
     * @param string $rootTagName The name of the root tag; this argument is optional
504
     *                            (default: 'root').
505
     * @param string $rowTagName  The name of the tag used for each row; this
506
     *                            argument is optional (default: 'row').
507
     *
508
     * @return DOMDocument        A new instance of DOMDocument.
509
     * @throws XML_Query2XML_Exception This is the base class for the exception
510
     *                            types XML_Query2XML_DBException and
511
     *                            XML_Query2XML_XMLException. By catching
512
     *                            XML_Query2XML_Exception you can catch all
513
     *                            exceptions this method will ever throw.
514
     * @throws XML_Query2XML_DBException If a database error occurrs.
515
     * @throws XML_Query2XML_XMLException If an XML error occurrs - most likely
516
     *                            $rootTagName or $rowTagName is not a valid
517
     *                            element name.
518
     */
519
    public function getFlatXML($sql, $rootTagName = 'root', $rowTagName = 'row')
520
    {
521
        /*
522
         * unit tests: getFlatXML/*.phpt
523
         */
524
        $dom     = self::_createDOMDocument();
525
        $rootTag = self::_addNewDOMChild($dom, $rootTagName, 'getFlatXML');
526
        $records = $this->_getAllRecords(array('query' => $sql), 'getFlatXML', $sql);
527
        foreach ($records as $record) {
528
            $rowTag = self::_addNewDOMChild($rootTag, $rowTagName, 'getFlatXML');
529
            foreach ($record as $field => $value) {
530
                self::_addNewDOMChild(
531
                    $rowTag,
532
                    $field,
533
                    'getFlatXML',
534
                    self::_utf8encode($value)
535
                );
536
            }
537
        }
538
        return $dom;
539
    }
540
 
541
    /**
542
     * Transforms your SQL data retrieved by one or more queries into complex and
543
     * highly configurable XML data.
544
     *
545
     * This method will return a new instance of DOMDocument.
546
     * Please see the <b>{@tutorial XML_Query2XML.pkg tutorial}</b> for details.
547
     *
548
     * @param mixed $sql     A string an array or the boolean value false.
549
     * @param array $options Options for the creation of the XML data stored in an
550
     *                       associative, potentially mutli-dimensional array
551
     *                       (please see the tutorial).
552
     *
553
     * @return DOMDocument   The XML data as a new instance of DOMDocument.
554
     * @throws XML_Query2XML_Exception This is the base class for the exception types
555
     *                       XML_Query2XML_DBException, XML_Query2XML_XMLException
556
     *                       and XML_Query2XML_ConfigException. By catching
557
     *                       XML_Query2XML_Exception you can catch all exceptions
558
     *                       this method will ever throw.
559
     * @throws XML_Query2XML_DBException If a database error occurrs.
560
     * @throws XML_Query2XML_XMLException If an XML error occurrs - most likely
561
     *                       an invalid XML element name.
562
     * @throws XML_Query2XML_ConfigException If some configuration options passed
563
     *                       as second argument are invalid or missing.
564
     */
565
    public function getXML($sql, $options)
566
    {
567
        /*
568
        * unit tests: getXML/*.phpt
569
        */
570
 
571
        // the default root tag name is 'root'
572
        if (isset($options['rootTag'])) {
573
            $rootTagName = $options['rootTag'];
574
        } else {
575
            $rootTagName = 'root';
576
        }
577
 
578
        $dom     = self::_createDOMDocument();
579
        $rootTag = self::_addNewDOMChild($dom, $rootTagName, '[rootTag]');
580
 
581
        $options['sql'] = $sql;
582
 
583
        if ($options['sql'] === false) {
584
            $options['sql'] = '';
585
        }
586
        $this->_preprocessOptions($options);
587
 
588
        /* Used to store the information which element has been created
589
        *  for which ID column value.
590
        */
591
        $tree = array();
592
 
593
        if ($sql === false) {
594
            $records = array(array()); // one empty record
595
        } else {
596
            $records = $this->_applySqlOptionsToRecord(
597
                $options,
598
                $emptyRecord = array()
599
            );
600
        }
601
 
602
        foreach ($records as $key => $record) {
603
            $tag = $this->_getNestedXMLRecord($records[$key], $options, $dom, $tree);
604
 
605
            /* _getNestedXMLRecord() returns false if an element already existed for
606
            *  the current ID column value.
607
            */
608
            if ($tag !== false) {
609
                $rootTag->appendChild($tag);
610
            }
611
        }
612
 
613
        $this->_stopDBProfiling();
614
 
615
        self::_removeContainers(
616
            $dom,
617
            $this->getGlobalOption('hidden_container_prefix')
618
        );
619
        return $dom;
620
    }
621
 
622
    /**
623
     * Perform pre-processing on $options.
624
     * This is a recursive method; it will call itself for every complex element
625
     * specification and every complex attribute specification found.
626
     *
627
     * @param array  &$options An associative array
628
     * @param string $context  Indecates whether an element or an attribute is
629
     *                         to be processed.
630
     *
631
     * @return void
632
     * @throws XML_Query2XML_ConfigException If a mandatory option is missing
633
     *                       or any option is defined incorrectly.
634
     */
635
    private function _preprocessOptions(&$options, $context = 'elements')
636
    {
637
        if (!isset($options['--q2x--path'])) {
638
            // things to do only at the root level
639
            $options['--q2x--path'] = '';
640
 
641
            if (!isset($options['rowTag'])) {
642
                $options['rowTag'] = 'row';
643
            }
644
 
645
            if (!isset($options['idColumn'])) {
646
                /*
647
                * unit test: _preprocessOptions/
648
                *  throwConfigException_idcolumnOptionMissing.phpt
649
                */
650
                throw new XML_Query2XML_ConfigException(
651
                    'The configuration option "idColumn" '
652
                    . 'is missing on the root level.'
653
                );
654
            }
655
        }
656
 
657
        foreach (array('encoder', 'mapper') as $option) {
658
            if (isset($options[$option])) {
659
                if (
660
                    is_string($options[$option]) &&
661
                    strpos($options[$option], '::') !== false
662
                ) {
663
                    $options[$option] = explode('::', $options[$option]);
664
                }
665
                if (
666
                    $options[$option] !== false
667
                    &&
668
                    !($option == 'encoder' && $options[$option] === null)
669
                    &&
670
                    !($option == 'mapper' && $options[$option] == false)
671
                    &&
672
                    !is_callable($options[$option], false, $callableName)
673
                ) {
674
                    /*
675
                    * Only check whether $options['encoder'] is callable if it's not
676
                    * set to:
677
                    * - false (don't use an encoder)
678
                    * - null (use self::_utf8encode()).
679
                    *
680
                    * unit test: _preprocessOptions/
681
                    *  throwConfigException_encoderNotCallableStaticMethod1.phpt
682
                    *  throwConfigException_encoderNotCallableStaticMethod2.phpt
683
                    *  throwConfigException_encoderNotCallableNonstaticMethod.phpt
684
                    *  throwConfigException_encoderNotCallableFunction.phpt
685
                    *
686
                    *
687
                    * Only check whether $options['mapper'] is callable if
688
                    * - $options['mapper'] != false
689
                    *
690
                    * unit tests: _preprocessOptions/
691
                    *  throwConfigException_mapperNotCallableStaticMethod1.phpt
692
                    *  throwConfigException_mapperNotCallableStaticMethod2.phpt
693
                    *  throwConfigException_mapperNotCallableNonstaticMethod.phpt
694
                    *  throwConfigException_mapperNotCallableFunction.phpt
695
                    */
696
                    throw new XML_Query2XML_ConfigException(
697
                        $options['--q2x--path'] . '[' . $option . ']: The '
698
                        . 'method/function "' . $callableName . '" is not callable.'
699
                    );
700
                }
701
            } else {
702
                $options[$option] = null;
703
            }
704
        }
705
 
706
        if ($context == 'elements') {
707
            foreach (array('elements', 'attributes') as $option) {
708
                if (isset($options[$option])) {
709
                    if (!is_array($options[$option])) {
710
                        /*
711
                        * unit test: _preprocessOptions/
712
                        *  throwConfigException_attributesOptionWrongType.phpt
713
                        *  throwConfigException_elementsOptionWrongType.phpt
714
                        */
715
                        throw new XML_Query2XML_ConfigException(
716
                            $options['--q2x--path'] . '[' . $option . ']: '
717
                            . 'array expected, ' . gettype($options[$option])
718
                            . ' given.'
719
                        );
720
                    }
721
                    foreach ($options[$option] as $key => $columnStr) {
722
                        $configPath = $options['--q2x--path'] . '[' . $option
723
                                      . '][' . $key . ']';
724
                        if (is_string($columnStr)) {
725
                            $options[$option][$key] =
726
                                $this->_buildCommandChain($columnStr, $configPath);
727
                            if (
728
                                is_numeric($key) &&
729
                                is_object($options[$option][$key])
730
                            ) {
731
                                /*
732
                                 * unit test: _preprocessOptions/
733
                                 *  throwConfigException_prefix_noArrayKey.phpt
734
                                 */
735
                                throw new XML_Query2XML_ConfigException(
736
                                    $configPath . ': the element name has to be '
737
                                    . 'specified as the array key when prefixes '
738
                                    . 'are used within the value specification'
739
                                );
740
                            }
741
                        } elseif (is_array($columnStr)) {
742
                            $options[$option][$key]['--q2x--path'] = $configPath;
743
 
744
                            // encoder option used by elements as well as attributes
745
                            if (
746
                                !array_key_exists(
747
                                    'encoder',
748
                                    $options[$option][$key]
749
                                 )
750
                            ) {
751
                                $options[$option][$key]['encoder'] =
752
                                    $options['encoder'];
753
                            }
754
                            if ($option == 'elements') {
755
                                // these options are only used by elements
756
                                if (
757
                                    !isset($options[$option][$key]['rootTag']) ||
758
                                    $options[$option][$key]['rootTag'] == ''
759
                                ) {
760
                                    /*
761
                                     * If rootTag is not set or an empty string:
762
                                     * create a hidden root tag
763
                                     */
764
                                    $options[$option][$key]['rootTag'] =
765
                                        $this->getGlobalOption(
766
                                            'hidden_container_prefix'
767
                                        ) . $key;
768
                                }
769
                                if (!isset($options[$option][$key]['rowTag'])) {
770
                                    $options[$option][$key]['rowTag'] = $key;
771
                                }
772
 
773
                                foreach (array('mapper', 'idColumn') as $option2) {
774
                                    if (
775
                                        !array_key_exists(
776
                                            $option2,
777
                                            $options[$option][$key]
778
                                        )
779
                                    ) {
780
                                        $options[$option][$key][$option2] =
781
                                            $options[$option2];
782
                                    }
783
                                }
784
                            }
785
                            $this->_preprocessOptions(
786
                                $options[$option][$key],
787
                                $option
788
                            );
789
                        } elseif (self::_isCallback($columnStr)) {
790
                            if (is_numeric($key)) {
791
                                /*
792
                                 * unit test: _preprocessOptions/
793
                                 *  throwConfigException_callbackInterface_
794
                                 *  noArrayKey.phpt
795
                                 */
796
                                throw new XML_Query2XML_ConfigException(
797
                                    $configPath . ': the element name has to be '
798
                                    . 'specified as the array key when the value '
799
                                    . 'is specified using an instance of '
800
                                    . 'XML_Query2XML_Callback.'
801
                                );
802
                            }
803
                        } else {
804
                            /*
805
                             * $columnStr is neither a string, an array or an
806
                             * instance of XML_Query2XML_Callback.
807
                             *
808
                             * unit tests:
809
                             *  _getNestedXMLRecord/
810
                             *   throwConfigException_attributeSpecWrongType.phpt
811
                             *  _preprocessOptions/
812
                             *   throwConfigException_callbackInterface_
813
                             *    complexAttributeSpec.phpt
814
                             *    simpleAttributeSpec.phpt
815
                             *    simpleElementSpec.phpt
816
                             */
817
                            throw new XML_Query2XML_ConfigException(
818
                                $configPath . ': array, string or instance of'
819
                                . ' XML_Query2XML_Callback expected, '
820
                                . gettype($columnStr)
821
                                . ' given.'
822
                            );
823
                        }
824
                    }
825
                }
826
            } // end of foreach (array('elements', 'attributes'))
827
        } else {
828
            // $context == 'attributes'
829
            if (!isset($options['value'])) {
830
                /*
831
                * the option "value" is mandatory
832
                * unit test: _preprocessOptions/
833
                *  throwConfigException_valueOptionMissing.phpt
834
                */
835
                throw new XML_Query2XML_ConfigException(
836
                    $options['--q2x--path'] . '[value]: Mandatory option "value" '
837
                    . 'missing from the complex attribute specification.'
838
                );
839
            }
840
        }
841
 
842
        $opt = array('value', 'condition', 'dynamicRowTag', 'idColumn');
843
        foreach ($opt as $option) {
844
            if (isset($options[$option])) {
845
                if (is_string($options[$option])) {
846
                    $options[$option] = $this->_buildCommandChain(
847
                        $options[$option],
848
                        $options['--q2x--path'] . '[value]'
849
                    );
850
                } elseif (
851
                    !self::_isCallback($options[$option]) &&
852
                    !($option == 'idColumn' && $options[$option] === false)
853
                ) {
854
                    /*
855
                    * unit tests:
856
                    *  _preprocessOptions/
857
                    *   throwConfigException_callbackInterface_
858
                    *    complexElementSpec.phpt
859
                    *    condition.phpt
860
                    *    idColumn.phpt
861
                    */
862
                    throw new XML_Query2XML_ConfigException(
863
                        $options['--q2x--path'] . '[' . $option . ']: string or'
864
                        . ' instance of XML_Query2XML_Callback expected, '
865
                        . gettype($options[$option])
866
                        . ' given.'
867
                    );
868
                }
869
            }
870
        }
871
 
872
        if (isset($options['query'])) {
873
            $options['sql'] = $options['query'];
874
        }
875
        if (isset($options['sql'])) {
876
 
877
            // we will pre-process $options['sql_options'] first
878
            if (isset($options['query_options'])) {
879
                $options['sql_options'] = $options['query_options'];
880
            }
881
            if (!isset($options['sql_options'])) {
882
                $options['sql_options'] = array();
883
            }
884
            $sql_options = array(
885
                'cached', 'single_record', 'merge', 'merge_master', 'merge_selective'
886
            );
887
            foreach ($sql_options as $option) {
888
                if (!isset($options['sql_options'][$option])) {
889
                    $options['sql_options'][$option] = false;
890
                }
891
            }
892
            if (isset($options['sql_options']['uncached'])) {
893
                $options['sql_options']['cached'] =
894
                    !$options['sql_options']['uncached'];
895
            }
896
 
897
            if ($options['sql_options']['cached']) {
898
                if (!is_array($options['sql'])) {
899
                    $options['sql'] = array('query' => $options['sql']);
900
                }
901
                if (isset($options['sql']['driver'])) {
902
                    $driver = $options['sql']['driver'];
903
                } else {
904
                    $driver = $this->_driver;
905
                }
906
                if (
907
                    !class_exists('XML_Query2XML_Driver_Caching') ||
908
                    !($driver instanceof XML_Query2XML_Driver_Caching)
909
                ) {
910
                    include_once 'XML/Query2XML/Driver/Caching.php';
911
                    $options['sql']['driver'] = new XML_Query2XML_Driver_Caching(
912
                        $driver
913
                    );
914
                }
915
            }
916
 
917
            if (
918
                $options['sql_options']['merge_selective'] !== false &&
919
                !is_array($options['sql_options']['merge_selective'])
920
            ) {
921
                /*
922
                * unit test: _preprocessOptions/
923
                *  throwConfigException_mergeselectiveOptionWrongType.phpt
924
                */
925
                throw new XML_Query2XML_ConfigException(
926
                    $options['--q2x--path'] . '[sql_options][merge_selective]: '
927
                    . 'array expected, '
928
                    . gettype($options['sql_options']['merge_selective']) . ' given.'
929
                );
930
            }
931
            // end of pre-processing of $options['sql_options']
932
 
933
            if (
934
                is_array($options['sql']) &&
935
                isset($options['sql']['driver']) &&
936
                $options['sql']['driver'] instanceof XML_Query2XML_Driver
937
            ) {
938
                $query = $options['sql']['driver']->preprocessQuery(
939
                    $options['sql'],
940
                    $options['--q2x--path'] . '[sql]'
941
                );
942
            } else {
943
                $query = $this->_driver->preprocessQuery(
944
                    $options['sql'],
945
                    $options['--q2x--path'] . '[sql]'
946
                );
947
            }
948
            $options['--q2x--query_statement'] = $query;
949
            if (
950
                is_array($options['sql']) &&
951
                isset($options['sql']['driver']) &&
952
                !($options['sql']['driver'] instanceof XML_Query2XML_Driver)
953
            ) {
954
                /*
955
                 * unit test: _preprocessOptions
956
                 *  throwConfigException_sqlOptionWrongType.phpt
957
                 */
958
                throw new XML_Query2XML_ConfigException(
959
                    $options['--q2x--path'] . '[sql][driver]: '
960
                    . 'instance of XML_Query2XML_Driver expected, '
961
                    . gettype($options['sql']['driver']) . ' given.'
962
                );
963
            }
964
 
965
            if (is_array($options['sql'])) {
966
                if (isset($options['sql']['data'])) {
967
                    if (is_array($options['sql']['data'])) {
968
                        foreach ($options['sql']['data'] as $key => $data) {
969
                            if (is_string($data)) {
970
                                $options['sql']['data'][$key] =
971
                                    $this->_buildCommandChain(
972
                                        $options['sql']['data'][$key],
973
                                        $options['--q2x--path']
974
                                            . '[sql][data][' . $key . ']'
975
                                    );
976
                            } elseif (!self::_isCallback($data)) {
977
                                /*
978
                                * unit tests: _preprocessOptions/
979
                                *   throwConfigException_callbackInterface_data.phpt
980
                                */
981
                                throw new XML_Query2XML_ConfigException(
982
                                    $options['--q2x--path'] . '[sql][data][' . $key
983
                                    . ']: string or'
984
                                    . ' instance of XML_Query2XML_Callback expected,'
985
                                    . ' ' . gettype($options['sql']['data'][$key])
986
                                    . ' given.'
987
                                );
988
                            }
989
                        }
990
                    } else {
991
                        /*
992
                        * unit test: _preprocessOptions/
993
                        *  throwConfigException_dataOptionWrongType.phpt
994
                        */
995
                        throw new XML_Query2XML_ConfigException(
996
                            $options['--q2x--path'] . '[sql][data]: array expected, '
997
                            . gettype($options['sql']['data']) . ' given.'
998
                        );
999
                    }
1000
                }
1001
            }
1002
        } // end of if (isset($options['sql'])
1003
    }
1004
 
1005
    /**
1006
     * Private recursive method that creates the nested XML elements from a record.
1007
     *
1008
     * getXML calls this method for every row in the initial result set.
1009
     * The $tree argument deserves some more explanation. All DOMNodes are stored
1010
     * in $tree the way they appear in the XML document. The same hirachy needs to be
1011
     * built so that we can know if a DOMNode that corresponds to a column ID of 2 is
1012
     * already a child node of a certain XML element. Let's have a look at an example
1013
     * to clarify this:
1014
     * <code>
1015
     * <music_library>
1016
     *   <artist>
1017
     *     <artistid>1</artistid>
1018
     *     <albums>
1019
     *       <album>
1020
     *         <albumid>1</albumid>
1021
     *       </album>
1022
     *       <album>
1023
     *         <albumid>2</albumid>
1024
     *       </album>
1025
     *     </albums>
1026
     *   </artist>
1027
     *   <artist>
1028
     *     <artistid>3</artistid>
1029
     *     <albums />
1030
     *   </artist>
1031
     * </music_library>
1032
     * </code>
1033
     * would be represended in the $tree array as something like this:
1034
     * <code>
1035
     * array (
1036
     *   [1] => array (
1037
     *     [tag] => DOMElement Object
1038
     *     [elements] => array (
1039
     *       [albums] => array (
1040
     *         [1] => array (
1041
     *           [tag] => DOMElement Object
1042
     *         )
1043
     *         [2] => array (
1044
     *           [tag] => DOMElement Object
1045
     *         )
1046
     *       )
1047
     *     )
1048
     *   )
1049
     *   [2] => array (
1050
     *     [tag] => DOMElement Object
1051
     *     [elements] => array
1052
     *     (
1053
     *       [albums] => array ()
1054
     *     )
1055
     *   )
1056
     * )
1057
     * </code>
1058
     * The numbers in the square brackets are column ID values.
1059
     *
1060
     * @param array       $record   An associative array representing a record;
1061
     *                              column names must be used as keys.
1062
     * @param array       &$options An array containing the options for this nested
1063
     *                              element; this will be a subset of the array
1064
     *                              originally passed to getXML().
1065
     * @param DOMDocument $dom      An instance of DOMDocument.
1066
     * @param array       &$tree    An associative multi-dimensional array, that is
1067
     *                              used to store the information which tag has
1068
     *                              already been created for a certain ID column
1069
     *                              value. It's format is:
1070
     *                              Array(
1071
     *                                "$id1" => Array(
1072
     *                                  'tag' => DOMElement,
1073
     *                                  'elements' => Array(
1074
     *                                    "$id2" => Array(
1075
     *                                      'tag' => DOMElement,
1076
     *                                      'elements' => Array( ... )
1077
     *                                    ),
1078
     *                                    "$id3" => ...
1079
     *                                  )
1080
     *                                )
1081
     *                              )
1082
     *
1083
     * @return mixed           The XML element's representation as a new instance of
1084
     *                         DOMNode or the boolean value false (meaning no
1085
     *                         new tag was created).
1086
     * @throws XML_Query2XML_DBException  Bubbles up through this method if thrown by
1087
     *                         - _processComplexElementSpecification()
1088
     * @throws XML_Query2XML_XMLException Bubbles up through this method if thrown by
1089
     *                         - _createDOMElement()
1090
     *                         - _setDOMAttribute
1091
     *                         - _appendTextChildNode()
1092
     *                         - _addNewDOMChild()
1093
     *                         - _addDOMChildren()
1094
     *                         - _processComplexElementSpecification()
1095
     *                         - _expandShortcuts()
1096
     *                         - _executeEncoder()
1097
     * @throws XML_Query2XML_ConfigException  Thrown if
1098
     *                         - $options['idColumn'] is not set
1099
     *                         - $options['elements'] is set but not an array
1100
     *                         - $options['attributes'] is set but not an array
1101
     *                         Bubbles up through this method if thrown by
1102
     *                         - _applyColumnStringToRecord()
1103
     *                         - _processComplexElementSpecification()
1104
     *                         - _expandShortcuts()
1105
     * @throws XML_Query2XML_Exception  Bubbles up through this method if thrown by
1106
     *                         - _expandShortcuts()
1107
     *                         - _applyColumnStringToRecord()
1108
     */
1109
    private function _getNestedXMLRecord($record, &$options, $dom, &$tree)
1110
    {
1111
        // the default row tag name is 'row'
1112
        if (isset($options['dynamicRowTag'])) {
1113
            $rowTagName = $this->_applyColumnStringToRecord(
1114
                $options['dynamicRowTag'],
1115
                $record,
1116
                $options['--q2x--path'] . '[dynamicRowTag]'
1117
            );
1118
        } else {
1119
            $rowTagName = $options['rowTag'];
1120
        }
1121
 
1122
        if ($options['idColumn'] === false) {
1123
            static $uniqueIdCounter = 0;
1124
            $id = ++$uniqueIdCounter;
1125
        } else {
1126
            $id = $this->_applyColumnStringToRecord(
1127
                $options['idColumn'],
1128
                $record,
1129
                $options['--q2x--path'] . '[idColumn]'
1130
            );
1131
 
1132
            if ($id === null) {
1133
                // the ID column is NULL
1134
                return false;
1135
            } elseif (is_object($id) || is_array($id)) {
1136
                /*
1137
                * unit test: _getNestedXMLRecord/
1138
                *   throwConfigException_idcolumnOptionWrongTypeArray.phpt
1139
                *   throwConfigException_idcolumnOptionWrongTypeObject.phpt
1140
                */
1141
                throw new XML_Query2XML_ConfigException(
1142
                    $options['--q2x--path'] . '[idColumn]: Must evaluate to a '
1143
                    . 'value that is not an object or an array.'
1144
                );
1145
            }
1146
        }
1147
 
1148
        /* Is there already an identical tag (identity being determined by the
1149
        *  value of the ID-column)?
1150
        */
1151
        if (isset($tree[$id])) {
1152
            if (isset($options['elements'])) {
1153
                foreach ($options['elements'] as $tagName => $column) {
1154
                    if (is_array($column)) {
1155
                        $this->_processComplexElementSpecification(
1156
                            $record,
1157
                            $options['elements'][$tagName],
1158
                            $tree[$id],
1159
                            $tagName
1160
                        );
1161
                    }
1162
                }
1163
            }
1164
            /*
1165
            * We return false because $tree[$id]['tag'] is already
1166
            * a child of the parent element.
1167
            */
1168
            return false;
1169
        } else {
1170
            $tree[$id] = array();
1171
 
1172
            if (isset($options['value'])) {
1173
                $parsedValue = $this->_applyColumnStringToRecord(
1174
                    $options['value'],
1175
                    $record,
1176
                    $options['--q2x--path'] . '[value]'
1177
                );
1178
                if (!$this->_evaluateCondition($parsedValue, $options['value'])) {
1179
                    // this element is to be skipped
1180
                    return false;
1181
                }
1182
            }
1183
            if (isset($options['condition'])) {
1184
                $continue = $this->_applyColumnStringToRecord(
1185
                    $options['condition'],
1186
                    $record,
1187
                    $options['--q2x--path'] . '[condition]'
1188
                );
1189
                if (!$continue) {
1190
                    // this element is to be skipped
1191
                    return false;
1192
                }
1193
            }
1194
            $tree[$id]['tag'] = self::_createDOMElement(
1195
                $dom,
1196
                $rowTagName,
1197
                $options['--q2x--path'] . '[rowTag/dynamicRowTag]'
1198
            );
1199
 
1200
            $tag = $tree[$id]['tag'];
1201
 
1202
            // add attributes
1203
            if (isset($options['attributes'])) {
1204
                if (!isset($options['processed'])) {
1205
                    $options['attributes'] = self::_expandShortcuts(
1206
                        $options['attributes'],
1207
                        $record,
1208
                        $options['mapper'],
1209
                        $options['--q2x--path'] . '[attributes]'
1210
                    );
1211
                }
1212
                foreach ($options['attributes'] as $attributeName => $column) {
1213
                    if (is_array($column)) {
1214
                        // complex attribute specification
1215
                        $this->_processComplexAttributeSpecification(
1216
                            $attributeName, $record, $column, $tree[$id]['tag']
1217
                        );
1218
                    } else {
1219
                        // simple attribute specifications
1220
                        $attributeValue = $this->_applyColumnStringToRecord(
1221
                            $column,
1222
                            $record,
1223
                            $options['--q2x--path']
1224
                            . '[attributes][' . $attributeName . ']'
1225
                        );
1226
                        if ($this->_evaluateCondition($attributeValue, $column)) {
1227
                            self::_setDOMAttribute(
1228
                                $tree[$id]['tag'],
1229
                                $attributeName,
1230
                                self::_executeEncoder(
1231
                                    $attributeValue,
1232
                                    $options
1233
                                ),
1234
                                $options['--q2x--path']
1235
                                . '[attributes][' . $attributeName . ']'
1236
                            );
1237
                        }
1238
                    }
1239
                }
1240
            }
1241
            if (isset($options['value'])) {
1242
                if ($parsedValue instanceof DOMNode || is_array($parsedValue)) {
1243
                    /*
1244
                    * The value returned from _applyColumnStringToRecord() and
1245
                    * stored in $parsedValue is an instance of DOMNode or an
1246
                    * array of DOMNode instances. _addDOMChildren() will handle
1247
                    * both.
1248
                    */
1249
                    self::_addDOMChildren(
1250
                        $tree[$id]['tag'],
1251
                        $parsedValue,
1252
                        $options['--q2x--path'] . '[value]',
1253
                        true
1254
                    );
1255
                } else {
1256
                    if ($parsedValue !== false && !is_null($parsedValue)) {
1257
                        self::_appendTextChildNode(
1258
                            $tree[$id]['tag'],
1259
                            self::_executeEncoder(
1260
                                $parsedValue,
1261
                                $options
1262
                            ),
1263
                            $options['--q2x--path'] . '[value]'
1264
                        );
1265
                    }
1266
                }
1267
            }
1268
 
1269
            // add child elements
1270
            if (isset($options['elements'])) {
1271
                if (!isset($options['processed'])) {
1272
                    $options['elements'] = self::_expandShortcuts(
1273
                        $options['elements'],
1274
                        $record,
1275
                        $options['mapper'],
1276
                        $options['--q2x--path'] . '[elements]'
1277
                    );
1278
                }
1279
                foreach ($options['elements'] as $tagName => $column) {
1280
                    if (is_array($column)) {
1281
                        // complex element specification
1282
                        $this->_processComplexElementSpecification(
1283
                            $record,
1284
                            $options['elements'][$tagName],
1285
                            $tree[$id],
1286
                            $tagName
1287
                        );
1288
                    } else {
1289
                        // simple element specification
1290
                        $tagValue = $this->_applyColumnStringToRecord(
1291
                            $column,
1292
                            $record,
1293
                            $options['--q2x--path'] . '[elements][' . $tagName . ']'
1294
                        );
1295
                        if ($this->_evaluateCondition($tagValue, $column)) {
1296
                            if (
1297
                                $tagValue instanceof DOMNode ||
1298
                                is_array($tagValue)
1299
                            ) {
1300
                                /*
1301
                                * The value returned from
1302
                                * _applyColumnStringToRecord() and stored in
1303
                                * $tagValue is an instance of DOMNode or an array
1304
                                * of DOMNode instances. self::_addDOMChildren()
1305
                                * will handle both.
1306
                                */
1307
                                self::_addDOMChildren(
1308
                                    self::_addNewDOMChild(
1309
                                        $tree[$id]['tag'],
1310
                                        $tagName,
1311
                                        $options['--q2x--path']
1312
                                        . '[elements][' . $tagName . ']'
1313
                                    ),
1314
                                    $tagValue,
1315
                                    $options['--q2x--path']
1316
                                    . '[elements][' . $tagName . ']',
1317
                                    true
1318
                                );
1319
                            } else {
1320
                                self::_addNewDOMChild(
1321
                                    $tree[$id]['tag'],
1322
                                    $tagName,
1323
                                    $options['--q2x--path']
1324
                                    . '[elements][' . $tagName . ']',
1325
                                    self::_executeEncoder(
1326
                                        $tagValue,
1327
                                        $options
1328
                                    )
1329
                                );
1330
                            }
1331
                        }
1332
                    }
1333
                }
1334
            }
1335
 
1336
            // some things only need to be done once
1337
            $options['processed'] = true;
1338
 
1339
            /*
1340
            *  We return $tree[$id]['tag'] because it needs to be added to it's
1341
            *  parent; this is to be handled by the method that called
1342
            *  _getNestedXMLRecord().
1343
            */
1344
            return $tree[$id]['tag'];
1345
        }
1346
    }
1347
 
1348
    /**
1349
     * Private method that will expand asterisk characters in an array
1350
     * of simple element specifications.
1351
     *
1352
     * This method gets called to handle arrays specified using the 'elements'
1353
     * or the 'attributes' option. An element specification that contains an
1354
     * asterisk will be duplicated for each column present in $record.
1355
     * Please see the {@tutorial XML_Query2XML.pkg tutorial} for details.
1356
     *
1357
     * @param Array  &$elements  An array of simple element specifications.
1358
     * @param Array  &$record    An associative array that represents a single
1359
     *                           record.
1360
     * @param mixed  $mapper     A valid argument for call_user_func(), a full method
1361
     *                           method name (e.g. "MyMapperClass::map") or a value
1362
     *                           that == false for no special mapping at all.
1363
     * @param string $configPath The config path; used for exception messages.
1364
     *
1365
     * @return Array The extended array.
1366
     * @throws XML_Query2XML_ConfigException If only the column part but not the
1367
     *                        explicitly defined tagName part contains an asterisk.
1368
     * @throws XML_Query2XML_Exception Will bubble up if it is thrown by
1369
     *                        _mapSQLIdentifierToXMLName(). This should never
1370
     *                        happen as _getNestedXMLRecord() already checks if
1371
     *                        $mapper is callable.
1372
     * @throws XML_Query2XML_XMLException Will bubble up if it is thrown by
1373
     *                        _mapSQLIdentifierToXMLName() which will happen if the
1374
     *                        $mapper function called, throws any exception.
1375
     */
1376
    private function _expandShortcuts(&$elements, &$record, $mapper, $configPath)
1377
    {
1378
        $newElements = array();
1379
        foreach ($elements as $tagName => $column) {
1380
            if (is_numeric($tagName)) {
1381
                $tagName = $column;
1382
            }
1383
            if (!is_array($column) && strpos($tagName, '*') !== false) {
1384
                // expand all occurences of '*' to all column names
1385
                foreach ($record as $columnName => $value) {
1386
                    $newTagName = str_replace('*', $columnName, $tagName);
1387
                    if (is_string($column)) {
1388
                        $newColumn = str_replace('*', $columnName, $column);
1389
                    } elseif (
1390
                        class_exists('XML_Query2XML_Data') &&
1391
                        $column instanceof XML_Query2XML_Data
1392
                    ) {
1393
                        $newColumn = clone $column;
1394
                        $callback  = $newColumn->getFirstPreProcessor();
1395
                        if (
1396
                            class_exists('XML_Query2XML_Data_Source') &&
1397
                            $callback instanceof XML_Query2XML_Data_Source
1398
                        ) {
1399
                            $callback->replaceAsterisks($columnName);
1400
                        }
1401
                    } else {
1402
                        $newColumn =& $column;
1403
                    }
1404
                    // do the mapping
1405
                    $newTagName = self::_mapSQLIdentifierToXMLName(
1406
                        $newTagName,
1407
                        $mapper,
1408
                        $configPath . '[' . $tagName . ']'
1409
                    );
1410
                    if (!isset($newElements[$newTagName])) {
1411
                        // only if the tagName hasn't already been used
1412
                        $newElements[$newTagName] = $newColumn;
1413
                    }
1414
                }
1415
            } else {
1416
                /*
1417
                * Complex element specifications will always be dealt with here.
1418
                * We don't want any mapping or handling of the asterisk shortcut
1419
                * to be done for complex element specifications.
1420
                */
1421
 
1422
                if (!is_array($column)) {
1423
                    // do the mapping but not for complex element specifications
1424
                    $tagName = self::_mapSQLIdentifierToXMLName(
1425
                        $tagName,
1426
                        $mapper,
1427
                        $configPath . '[' . $tagName . ']'
1428
                    );
1429
                }
1430
 
1431
                /*
1432
                 * explicit specification without an asterisk;
1433
                 * this always overrules an expanded asterisk
1434
                 */
1435
                unset($newElements[$tagName]);
1436
                $newElements[$tagName] = $column;
1437
            }
1438
        }
1439
        return $newElements;
1440
    }
1441
 
1442
    /**
1443
     * Maps an SQL identifier to an XML name using the supplied $mapper.
1444
     *
1445
     * @param string $sqlIdentifier The SQL identifier as a string.
1446
     * @param mixed  $mapper        A valid argument for call_user_func(), a full
1447
     *                              method method name (e.g. "MyMapperClass::map")
1448
     *                              or a value that == false for no special mapping
1449
     *                              at all.
1450
     * @param string $configPath    The config path; used for exception messages.
1451
     *
1452
     * @return string The mapped XML name.
1453
     * @throws XML_Query2XML_Exception If $mapper is not callable. This should never
1454
     *                              happen as _getNestedXMLRecord() already checks
1455
     *                              if $mapper is callable.
1456
     * @throws XML_Query2XML_XMLException If the $mapper function called, throws any
1457
     *                              exception.
1458
     */
1459
    private function _mapSQLIdentifierToXMLName($sqlIdentifier, $mapper, $configPath)
1460
    {
1461
        if (!$mapper) {
1462
            // no mapper was defined
1463
            $xmlName = $sqlIdentifier;
1464
        } else {
1465
            if (is_callable($mapper, false, $callableName)) {
1466
                try {
1467
                    $xmlName = call_user_func($mapper, $sqlIdentifier);
1468
                } catch (Exception $e) {
1469
                    /*
1470
                    * This will also catch XML_Query2XML_ISO9075Mapper_Exception
1471
                    * if $mapper was "XML_Query2XML_ISO9075Mapper::map".
1472
                    * unit test:
1473
                    *  _mapSQLIdentifierToXMLName/throwXMLException.phpt
1474
                    */
1475
                    throw new XML_Query2XML_XMLException(
1476
                        $configPath . ': Could not map "' . $sqlIdentifier
1477
                        . '" to an XML name using the mapper '
1478
                        . $callableName . ': ' . $e->getMessage()
1479
                    );
1480
                }
1481
            } else {
1482
                /*
1483
                * This should never happen as _preprocessOptions() already
1484
                * checks if $mapper is callable. Therefore no unit tests
1485
                * can be provided for this exception.
1486
                */
1487
                throw new XML_Query2XML_ConfigException(
1488
                    $configPath . ': The mapper "' . $callableName
1489
                    . '" is not callable.'
1490
                );
1491
            }
1492
        }
1493
        return $xmlName;
1494
    }
1495
 
1496
    /**
1497
     * Private method that processes a complex element specification
1498
     * for {@link XML_Query2XML::_getNestedXMLRecord()}.
1499
     *
1500
     * @param array  &$record  The current record.
1501
     * @param array  &$options The current options.
1502
     * @param array  &$tree    Associative multi-dimensional array, that is used to
1503
     *                         store which tags have already been created
1504
     * @param string $tagName  The element's name.
1505
     *
1506
     * @return void
1507
     * @throws XML_Query2XML_XMLException This exception will bubble up
1508
     *                        if it is thrown by _getNestedXMLRecord(),
1509
     *                        _applySqlOptionsToRecord() or _addDOMChildren().
1510
     * @throws XML_Query2XML_DBException  This exception will bubble up
1511
     *                        if it is thrown by _applySqlOptionsToRecord()
1512
     *                        or _getNestedXMLRecord().
1513
     * @throws XML_Query2XML_ConfigException This exception will bubble up
1514
     *                        if it is thrown by _applySqlOptionsToRecord()
1515
     *                        or _getNestedXMLRecord().
1516
     * @throws XML_Query2XML_Exception  This exception will bubble up if it
1517
     *                        is thrown by _getNestedXMLRecord().
1518
     */
1519
    private function _processComplexElementSpecification(&$record, &$options, &$tree,
1520
        $tagName)
1521
    {
1522
        $tag = $tree['tag'];
1523
        if (!isset($tree['elements'])) {
1524
            $tree['elements'] = array();
1525
        }
1526
        if (!isset($tree['elements'][$tagName])) {
1527
            $tree['elements'][$tagName]            = array();
1528
            $tree['elements'][$tagName]['rootTag'] = self::_addNewDOMChild(
1529
                $tag,
1530
                $options['rootTag'],
1531
                $options['--q2x--path'] . '[rootTag]'
1532
            );
1533
        }
1534
 
1535
        $records =& $this->_applySqlOptionsToRecord($options, $record);
1536
 
1537
        for ($i = 0; $i < count($records); $i++) {
1538
            self::_addDOMChildren(
1539
                $tree['elements'][$tagName]['rootTag'],
1540
                $this->_getNestedXMLRecord(
1541
                    $records[$i],
1542
                    $options,
1543
                    $tag->ownerDocument,
1544
                    $tree['elements'][$tagName]
1545
                ),
1546
                $options['--q2x--path']
1547
            );
1548
        }
1549
    }
1550
 
1551
    /**
1552
     * Private method that processes a complex attribute specification
1553
     * for {@link XML_Query2XML::_getNestedXMLRecord()}.
1554
     *
1555
     * A complex attribute specification consists of an associative array
1556
     * with the keys 'value' (mandatory), 'condition', 'sql' and 'sql_options'.
1557
     *
1558
     * @param string  $attributeName The name of the attribute as it was specified
1559
     *                               using the array key of the complex attribute
1560
     *                               specification.
1561
     * @param array   &$record       The current record.
1562
     * @param array   &$options      The complex attribute specification itself.
1563
     * @param DOMNode $tag           The DOMNode to which the attribute is to be
1564
     *                               added.
1565
     *
1566
     * @return void
1567
     * @throws XML_Query2XML_XMLException This exception will bubble up
1568
     *                          if it is thrown by _setDOMAttribute(),
1569
     *                          _applyColumnStringToRecord(),
1570
     *                          _applySqlOptionsToRecord() or _executeEncoder().
1571
     * @throws XML_Query2XML_DBException  This exception will bubble up
1572
     *                          if it is thrown by _applySqlOptionsToRecord().
1573
     * @throws XML_Query2XML_ConfigException This exception will bubble up
1574
     *                          if it is thrown by _applySqlOptionsToRecord() or
1575
     *                          _applyColumnStringToRecord(). It will also be thrown
1576
     *                          by this method if $options['value'] is not set.
1577
     */
1578
    private function _processComplexAttributeSpecification($attributeName, &$record,
1579
        &$options, $tag)
1580
    {
1581
        if (isset($options['condition'])) {
1582
            $continue = $this->_applyColumnStringToRecord(
1583
                $options['condition'],
1584
                $record,
1585
                $options['--q2x--path'] . '[condition]'
1586
            );
1587
            if (!$continue) {
1588
                // this element is to be skipped
1589
                return;
1590
            }
1591
        }
1592
 
1593
        // only fetching a single record makes sense for a single attribute
1594
        $options['sql_options']['single_record'] = true;
1595
 
1596
        $records = $this->_applySqlOptionsToRecord($options, $record);
1597
        if (count($records) == 0) {
1598
            /*
1599
            * $options['sql'] was set but the query did not return any records.
1600
            * Therefore this attribute is to be skipped.
1601
            */
1602
            return;
1603
        }
1604
        $attributeRecord = $records[0];
1605
 
1606
        $attributeValue = $this->_applyColumnStringToRecord(
1607
            $options['value'],
1608
            $attributeRecord,
1609
            $options['--q2x--path'] . '[value]'
1610
        );
1611
        if ($this->_evaluateCondition($attributeValue, $options['value'])) {
1612
            self::_setDOMAttribute(
1613
                $tag,
1614
                $attributeName,
1615
                self::_executeEncoder($attributeValue, $options),
1616
                $options['--q2x--path'] . '[value]'
1617
            );
1618
        }
1619
    }
1620
 
1621
    /**
1622
     * Private method to apply the givenen sql option to a record.
1623
     *
1624
     * This method handles the sql options 'single_record',
1625
     * 'merge', 'merge_master' and 'merge_selective'. Please see the
1626
     * {@tutorial XML_Query2XML.pkg tutorial} for details.
1627
     *
1628
     * @param array &$options An associative multidimensional array of options.
1629
     * @param array &$record  The current record as an associative array.
1630
     *
1631
     * @return array          An indexed array of records that are themselves
1632
     *                        represented as associative arrays.
1633
     * @throws XML_Query2XML_ConfigException This exception is thrown if
1634
     *                        - a column specified in merge_selective does not exist
1635
     *                          in the result set
1636
     *                        - it bubbles up from _applyColumnStringToRecord()
1637
     * @throws XML_Query2XML_DBException This exception will bubble up
1638
     *                        if it is thrown by _getAllRecords().
1639
     * @throws XML_Query2XML_XMLException It will bubble up if it is thrown
1640
     *                        by _applyColumnStringToRecord().
1641
     */
1642
    private function _applySqlOptionsToRecord(&$options, &$record)
1643
    {
1644
        if (!isset($options['sql'])) {
1645
            return array($record);
1646
        }
1647
 
1648
        $single_record   = $options['sql_options']['single_record'];
1649
        $merge           = $options['sql_options']['merge'];
1650
        $merge_master    = $options['sql_options']['merge_master'];
1651
        $merge_selective = $options['sql_options']['merge_selective'];
1652
 
1653
        $sql = $options['sql'];
1654
        if (is_array($sql)) {
1655
            if (isset($sql['data'])) {
1656
                foreach ($sql['data'] as $key => $columnStr) {
1657
                    $sql['data'][$key] = $this->_applyColumnStringToRecord(
1658
                        $columnStr,
1659
                        $record,
1660
                        $options['--q2x--path'] . '[sql][data][' . $key . ']'
1661
                    );
1662
                }
1663
            }
1664
        }
1665
        $sqlConfigPath = $options['--q2x--path'] . '[sql]';
1666
 
1667
        $records =& $this->_getAllRecords(
1668
            $sql,
1669
            $sqlConfigPath,
1670
            $options['--q2x--query_statement']
1671
        );
1672
        if ($single_record && isset($records[0])) {
1673
            $records = array($records[0]);
1674
        }
1675
 
1676
        if (is_array($merge_selective)) {
1677
            // selective merge
1678
            if ($merge_master) {
1679
                // current records are master
1680
                for ($ii = 0; $ii < count($merge_selective); $ii++) {
1681
                    for ($i = 0; $i < count($records); $i++) {
1682
                        if (!array_key_exists($merge_selective[$ii], $record)) {
1683
                            /* Selected field does not exist in the parent record
1684
                            * (passed as argumnet $record)
1685
                            * unit test: _applySqlOptionsToRecord/
1686
                            *  throwConfigException_mergeMasterTrue.phpt
1687
                            */
1688
                            throw new XML_Query2XML_ConfigException(
1689
                                $options['--q2x--path'] . '[sql_options]'
1690
                                . '[merge_selective]['. $ii . ']: The column "'
1691
                                . $merge_selective[$ii] . '" '
1692
                                . 'was not found in the result set.'
1693
                            );
1694
                        }
1695
                        if (!array_key_exists($merge_selective[$ii], $records[$i])) {
1696
                            // we are the master, so only if it does not yet exist
1697
                            $records[$i][$merge_selective[$ii]] =
1698
                                $record[$merge_selective[$ii]];
1699
                        }
1700
                    }
1701
                }
1702
            } else {
1703
                // parent record is master
1704
                for ($ii = 0; $ii < count($merge_selective); $ii++) {
1705
                    for ($i = 0; $i < count($records); $i++) {
1706
                        if (!array_key_exists($merge_selective[$ii], $record)) {
1707
                            /* Selected field does not exist in the parent record
1708
                            *  (passed as argumnet $record)
1709
                            *  unit test: _applySqlOptionsToRecord/
1710
                            *   throwConfigException_mergeMasterFalse.phpt
1711
                            */
1712
                            throw new XML_Query2XML_ConfigException(
1713
                                $options['--q2x--path'] . '[sql_options]'
1714
                                . '[merge_selective]['. $ii . ']: The column "'
1715
                                . $merge_selective[$ii] . '" '
1716
                                . 'was not found in the result set.'
1717
                            );
1718
                        }
1719
                        // parent is master!
1720
                        $records[$i][$merge_selective[$ii]] =
1721
                            $record[$merge_selective[$ii]];
1722
                    }
1723
                }
1724
            }
1725
        } elseif ($merge) {
1726
            // regular merge
1727
            if ($merge_master) {
1728
                for ($i = 0; $i < count($records); $i++) {
1729
                    $records[$i] = array_merge($record, $records[$i]);
1730
                }
1731
            } else {
1732
                for ($i = 0; $i < count($records); $i++) {
1733
                    $records[$i] = array_merge($records[$i], $record);
1734
                }
1735
            }
1736
        }
1737
        return $records;
1738
    }
1739
 
1740
    /**
1741
     * Private method to apply a column string to a record.
1742
     * Please see the tutorial for details on the different column strings.
1743
     *
1744
     * @param string $columnStr  A valid column name or an instance of a class
1745
     *                           implementing XML_Query2XML_Callback.
1746
     * @param array  &$record    The record as an associative array.
1747
     * @param string $configPath The config path; used for exception messages.
1748
     *
1749
     * @return mixed A value that can be cast to a string or an instance of DOMNode.
1750
     * @throws XML_Query2XML_ConfigException  Thrown if $columnStr is not
1751
     *               a string or an instance of XML_Query2XML_Callback or if
1752
     *               $record[$columnStr] does not exist (and $columnStr has
1753
     *               no special prefix).
1754
     * @throws XML_Query2XML_XMLException     Thrown if the '&' prefix was used
1755
     *               but the data was not unserializeable, i.e. not valid XML data.
1756
     */
1757
    private function _applyColumnStringToRecord($columnStr, &$record, $configPath)
1758
    {
1759
        if (self::_isCallback($columnStr)) {
1760
            $value = $columnStr->execute($record);
1761
        } elseif (is_string($columnStr)) {
1762
            if (array_key_exists($columnStr, $record)) {
1763
                $value = $record[$columnStr];
1764
            } else {
1765
                /*
1766
                * unit test:
1767
                *  _applyColumnStringToRecord/throwConfigException_element1.phpt
1768
                *  _applyColumnStringToRecord/throwConfigException_element2.phpt
1769
                *  _applyColumnStringToRecord/throwConfigException_idcolumn.phpt
1770
                */
1771
                throw new XML_Query2XML_ConfigException(
1772
                    $configPath . ': The column "' . $columnStr
1773
                    . '" was not found in the result set.'
1774
                );
1775
 
1776
            }
1777
        } else {
1778
            // should never be reached
1779
            throw new XML_Query2XML_ConfigException(
1780
                $configPath . ': string or instance of XML_Query2XML_Callback'
1781
                . ' expected, ' . gettype($columnStr) . ' given.'
1782
            );
1783
        }
1784
        return $value;
1785
    }
1786
 
1787
    /**
1788
     * Returns whether $value is to be included in the output.
1789
     * If $spec is a string an is prefixed by a question mark this method will
1790
     * return false if $value is null or is a string with a length of zero. In
1791
     * any other case, this method will return the true.
1792
     *
1793
     * @param string $value The value.
1794
     * @param mixed  $spec  The value specification. This can be a string
1795
     *                      or an instance of XML_Query2XML_Callback.
1796
     *
1797
     * @return boolean Whether $value is to be included in the output.
1798
     */
1799
    private function _evaluateCondition($value, $spec)
1800
    {
1801
        return !class_exists('XML_Query2XML_Data_Condition') ||
1802
               !$spec instanceof XML_Query2XML_Data_Condition ||
1803
               $spec->evaluateCondition($value);
1804
    }
1805
 
1806
    /**
1807
     * Private method to fetch all records from a result set.
1808
     *
1809
     * @param mixed  $sql            The SQL query as a string or an array.
1810
     * @param string $configPath     The config path; used for exception messages.
1811
     * @param string $queryStatement The query as a string; it will be used for
1812
     *                               logging and profiling.
1813
     *
1814
     * @return array An array of records. Each record itself will be an
1815
     *                   associative array.
1816
     */
1817
    private function &_getAllRecords($sql, $configPath, $queryStatement)
1818
    {
1819
        // $queryStatement will be used for profiling
1820
        if ($this->_profiling || $this->_debug) {
1821
            $loggingQuery = $queryStatement;
1822
            if (is_array($sql) && isset($sql['data']) && is_array($sql['data'])) {
1823
                $loggingQuery .= '; DATA:' . implode(',', $sql['data']);
1824
            }
1825
            $this->_debugStartQuery($loggingQuery, $queryStatement);
1826
        }
1827
 
1828
        if (is_array($sql) && isset($sql['driver'])) {
1829
            $driver = $sql['driver'];
1830
        } else {
1831
            $driver = $this->_driver;
1832
        }
1833
        $records = $driver->getAllRecords($sql, $configPath);
1834
 
1835
        $this->_debugStopQuery($queryStatement);
1836
        return $records;
1837
    }
1838
 
1839
    /**
1840
     * Initializes a query's profile (only used if profiling is turned on).
1841
     *
1842
     * @param mixed &$sql The SQL query as a string or an array.
1843
     *
1844
     * @return void
1845
     * @see startProfiling()
1846
     */
1847
    private function _initQueryProfile(&$sql)
1848
    {
1849
        if (!isset($this->_profile['queries'][$sql])) {
1850
            $this->_profile['queries'][$sql] = array(
1851
                'count' => 0,
1852
                'runTimes' => array()
1853
            );
1854
        }
1855
    }
1856
 
1857
    /**
1858
     * Starts the debugging and profiling of the query passed as argument.
1859
     *
1860
     * @param string $loggingQuery   The query statement as it will be logged.
1861
     * @param string $profilingQuery The query statement as it will be used for
1862
     *                               profiling.
1863
     *
1864
     * @return void
1865
     */
1866
    private function _debugStartQuery($loggingQuery, $profilingQuery)
1867
    {
1868
        $this->_debug('QUERY: ' . $loggingQuery);
1869
        if ($this->_profiling) {
1870
            $this->_initQueryProfile($profilingQuery);
1871
            ++$this->_profile['queries'][$profilingQuery]['count'];
1872
            $this->_profile['queries'][$profilingQuery]['runTimes'][] = array(
1873
                'start' => microtime(true),
1874
                'stop' => 0
1875
            );
1876
        }
1877
    }
1878
 
1879
    /**
1880
     * Ends the debugging and profiling of the query passed as argument.
1881
     *
1882
     * @param string $profilingQuery The query statement as it will be used for
1883
     *                               profiling.
1884
     *
1885
     * @return void
1886
     */
1887
    private function _debugStopQuery($profilingQuery)
1888
    {
1889
        $this->_debug('DONE');
1890
        if ($this->_profiling) {
1891
            $this->_initQueryProfile($profilingQuery);
1892
            $lastIndex =
1893
                count(
1894
                    $this->_profile['queries'][$profilingQuery]['runTimes']
1895
                ) - 1;
1896
 
1897
            $this->_profile['queries'][$profilingQuery]['runTimes'][$lastIndex]['stop'] =
1898
                microtime(true);
1899
        }
1900
    }
1901
 
1902
    /**
1903
     * Stops the DB profiling.
1904
     * This will set $this->_profile['dbDuration'].
1905
     *
1906
     * @return void
1907
     */
1908
    private function _stopDBProfiling()
1909
    {
1910
        if ($this->_profiling && isset($this->_profile['start'])) {
1911
            $this->_profile['dbStop']     = microtime(1);
1912
            $this->_profile['dbDuration'] =
1913
                $this->_profile['dbStop'] - $this->_profile['start'];
1914
        }
1915
    }
1916
 
1917
    /**
1918
     * Private method used to log debug messages.
1919
     * This method will do no logging if $this->_debug is set to false.
1920
     *
1921
     * @param string $msg The message to log.
1922
     *
1923
     * @return void
1924
     * @see _debugLogger
1925
     * @see _debug
1926
     */
1927
    private function _debug($msg)
1928
    {
1929
        if ($this->_debug) {
1930
            $this->_debugLogger->log($msg);
1931
        }
1932
    }
1933
 
1934
    /**
1935
     * Returns whether $object is an instance of XML_Query2XML_Callback.
1936
     *
1937
     * @param mixed $object The variable to check.
1938
     *
1939
     * @return boolean
1940
     */
1941
    private static function _isCallback($object)
1942
    {
1943
        return is_object($object) &&
1944
               interface_exists('XML_Query2XML_Callback') &&
1945
               $object instanceof XML_Query2XML_Callback;
1946
    }
1947
 
1948
    /**
1949
     * Parse specifications that use the prifixes ?, &, =, ^, :,  or #.
1950
     *
1951
     * This method will produce a number of chained Data Class objects all of
1952
     * which be an instance of the abstract class XML_Query2XML_Data.
1953
     *
1954
     * @param string $columnStr  The original specification.
1955
     * @param string $configPath The config path; used for exception messages.
1956
     *
1957
     * @return mixed An instance of XML_Query2XML_Callback or a column
1958
     *               name as a string.
1959
     * @throws XML_Query2XML_ConfigException Bubbles up through this method if
1960
     *                                       thrown by any of the command class
1961
     *                                       constructors.
1962
     */
1963
    private function _buildCommandChain($columnStr, $configPath)
1964
    {
1965
        $prefixList = implode('', array_keys($this->_prefixes));
1966
        if (ltrim($columnStr, $prefixList) == $columnStr) {
1967
            return $columnStr;
1968
        }
1969
 
1970
        $firstCallback = null;
1971
        for ($i = 0; $i < strlen($columnStr); $i++) {
1972
            $prefix = substr($columnStr, $i, 1);
1973
            if (isset($this->_prefixes[$prefix])) {
1974
                $columnSubStr = substr($columnStr, $i + 1);
1975
                $filePath     = $this->_prefixes[$prefix][0];
1976
                $className    = $this->_prefixes[$prefix][1];
1977
                if ($columnSubStr === false) {
1978
                    $columnSubStr = '';
1979
                }
1980
 
1981
                if ($filePath) {
1982
                    include_once $filePath;
1983
                }
1984
 
1985
                if (!in_array(
1986
                        'XML_Query2XML_Data',
1987
                        class_parents($className)
1988
                    )
1989
                ) {
1990
                    throw new XML_Query2XML_ConfigException(
1991
                        $configPath . ': Prefix class ' . $className . ' does ' .
1992
                        'not extend XML_Query2XML_Data.'
1993
                    );
1994
                }
1995
 
1996
                if (in_array(
1997
                        'XML_Query2XML_Data_Source',
1998
                        class_parents($className)
1999
                    )
2000
                ) {
2001
                    // data source prefix
2002
                    $callback = call_user_func_array(
2003
                        array($className, 'create'),
2004
                        array($columnSubStr, $configPath)
2005
                    );
2006
                } else {
2007
                    // data processing prefix
2008
                    $callback = call_user_func_array(
2009
                        array($className, 'create'),
2010
                        array(null, $configPath)
2011
                    );
2012
 
2013
                    if (ltrim($columnSubStr, $prefixList) == $columnSubStr) {
2014
                        // no more prefixes: ColumnValue is the default data source
2015
                        include_once 'XML/Query2XML/Data/Source/ColumnValue.php';
2016
                        $callback->setPreProcessor(
2017
                            new XML_Query2XML_Data_Source_ColumnValue(
2018
                                $columnSubStr,
2019
                                $configPath
2020
                            )
2021
                        );
2022
                    }
2023
                }
2024
 
2025
                if (is_null($firstCallback)) {
2026
                    $firstCallback = $callback;
2027
                } else {
2028
                    if (
2029
                        $callback instanceof XML_Query2XML_Data_Condition &&
2030
                        !($firstCallback instanceof XML_Query2XML_Data_Condition)
2031
                    ) {
2032
                        throw new XML_Query2XML_ConfigException(
2033
                            $configPath . ': conditional prefixes always have to '
2034
                            . 'go first.'
2035
                        );
2036
                    }
2037
                    $firstCallback->getFirstPreProcessor()->setPreProcessor(
2038
                        $callback
2039
                    );
2040
                }
2041
                if (
2042
                    $firstCallback->getFirstPreProcessor()
2043
                    instanceof XML_Query2XML_Data_Source
2044
                ) {
2045
                    // there can only be one data source
2046
                    break;
2047
                }
2048
            } else {
2049
                break;
2050
            }
2051
        }
2052
        if (is_null($firstCallback)) {
2053
            return $columnStr;
2054
        } else {
2055
            return $firstCallback;
2056
        }
2057
    }
2058
 
2059
    /**
2060
     * Creates a new instance of DOMDocument.
2061
     * '1.0' is passed as first argument and 'UTF-8' as second to the
2062
     * DOMDocument constructor.
2063
     *
2064
     * @return DOMDocument The new instance.
2065
     */
2066
    private static function _createDOMDocument()
2067
    {
2068
        return new DOMDocument('1.0', 'UTF-8');
2069
    }
2070
 
2071
    /**
2072
     * Create and then add a new child element.
2073
     *
2074
     * @param DOMNode $element    The parent DOMNode the new DOM element should be
2075
     *                            appended to.
2076
     * @param string  $name       The tag name of the new element.
2077
     * @param string  $configPath The config path; used for exception messages.
2078
     * @param string  $value      The value of a child text node. This argument is
2079
     *                            optional. The default is the boolean value false,
2080
     *                            which means that no child text node will be
2081
     *                            appended.
2082
     *
2083
     * @return DOMNode The newly created DOMNode instance that was appended
2084
     *                 to $element.
2085
     * @throws XML_Query2XML_XMLException This exception will bubble up if it is
2086
     *                 thrown by _createDOMElement().
2087
     */
2088
    private static function _addNewDOMChild(DOMNode $element, $name, $configPath,
2089
        $value = false)
2090
    {
2091
        if ($element instanceof DOMDocument) {
2092
            $dom = $element;
2093
        } else {
2094
            $dom = $element->ownerDocument;
2095
        }
2096
        $child = self::_createDOMElement($dom, $name, $configPath, $value);
2097
        $element->appendChild($child);
2098
        return $child;
2099
    }
2100
 
2101
    /**
2102
     * Helper method to create a new instance of DOMNode
2103
     *
2104
     * @param DOMDocument $dom        An instance of DOMDocument. It's
2105
     *                                createElement() method is used to create the
2106
     *                                new DOMNode instance.
2107
     * @param string      $name       The tag name of the new element.
2108
     * @param string      $configPath The config path; used for exception messages.
2109
     * @param string      $value      The value of a child text node. This argument
2110
     *                                is optional. The default is the boolean value
2111
     *                                false, which means that no child text node will
2112
     *                                be appended.
2113
     *
2114
     * @return DOMNode An instance of DOMNode.
2115
     * @throws XML_Query2XML_XMLException If $name is an invalid XML identifier.
2116
     *                                    Also it will bubble up if it is thrown by
2117
     *                                    _appendTextChildNode().
2118
     */
2119
    private static function _createDOMElement(DOMDocument $dom, $name, $configPath,
2120
        $value = false)
2121
    {
2122
        try {
2123
            $element = $dom->createElement($name);
2124
        } catch(DOMException $e) {
2125
            /*
2126
            * unit tests:
2127
            *  _createDOMElement/throwXMLException_elementInvalid1.phpt
2128
            *  _createDOMElement/throwXMLException_elementInvalid2.phpt
2129
            *  _createDOMElement/throwXMLException_roottagOptionInvalid1.phpt
2130
            *  _createDOMElement/throwXMLException_roottagOptionInvalid2.phpt
2131
            *  _createDOMElement/throwXMLException_rowtagOptionInvalid.phpt
2132
            */
2133
            throw new XML_Query2XML_XMLException(
2134
                $configPath . ': "' . $name . '" is an invalid XML element name: '
2135
                . $e->getMessage(),
2136
                $e
2137
            );
2138
        }
2139
        self::_appendTextChildNode($element, $value, $configPath);
2140
        return $element;
2141
    }
2142
 
2143
    /**
2144
     * Append a new child text node to $element.
2145
     * $value must already be UTF8-encoded; this is to be handled
2146
     * by self::_executeEncoder() and $options['encoder'].
2147
     *
2148
     * This method will not create and append a child text node
2149
     * if $value === false || is_null($value).
2150
     *
2151
     * @param DOMNode $element    An instance of DOMNode
2152
     * @param string  $value      The value of the text node.
2153
     * @param string  $configPath The config path; used for exception messages.
2154
     *
2155
     * @return void
2156
     * @throws XML_Query2XML_XMLException Any lower-level DOMException will
2157
     *                 wrapped and re-thrown as a XML_Query2XML_XMLException. This
2158
     *                 will happen if $value cannot be UTF8-encoded for some reason.
2159
     *                 It will also be thrown if $value is an object or an array
2160
     *                 (and can therefore not be converted into a string).
2161
     */
2162
    private static function _appendTextChildNode(DOMNode $element,
2163
                                                 $value,
2164
                                                 $configPath)
2165
    {
2166
        if ($value === false || is_null($value)) {
2167
            return;
2168
        } elseif (is_object($value) || is_array($value)) {
2169
            /*
2170
            * Objects and arrays cannot be cast
2171
            * to a string without an error.
2172
            *
2173
            * unit test:
2174
            * _appendTextChildNode/throwXMLException.phpt
2175
            */
2176
            throw new XML_Query2XML_XMLException(
2177
                $configPath . ': A value of the type ' . gettype($value)
2178
                . ' cannot be used for a text node.'
2179
            );
2180
        }
2181
        $dom = $element->ownerDocument;
2182
        try {
2183
            $element->appendChild($dom->createTextNode($value));
2184
        } catch(DOMException $e) {
2185
            // this should never happen as $value is UTF-8 encoded
2186
            throw new XML_Query2XML_XMLException(
2187
                $configPath . ': "' . $value . '" is not a vaild text node: '
2188
                . $e->getMessage(),
2189
                $e
2190
            );
2191
        }
2192
    }
2193
 
2194
    /**
2195
     * Set the attribute $name with a value of $value for $element.
2196
     * $value must already be UTF8-encoded; this is to be handled
2197
     * by self::_executeEncoder() and $options['encoder'].
2198
     *
2199
     * @param DOMNode $element    An instance of DOMNode
2200
     * @param string  $name       The name of the attribute to set.
2201
     * @param string  $value      The value of the attribute to set.
2202
     * @param string  $configPath The config path; used for exception messages.
2203
     *
2204
     * @return void
2205
     * @throws XML_Query2XML_XMLException Any lower-level DOMException will be
2206
     *                 wrapped and re-thrown as a XML_Query2XML_XMLException. This
2207
     *                 will happen if $name is not a valid attribute name. It will
2208
     *                 also be thrown if $value is an object or an array (and can
2209
     *                 therefore not be converted into a string).
2210
     */
2211
    private static function _setDOMAttribute(DOMNode $element,
2212
                                             $name,
2213
                                             $value,
2214
                                             $configPath)
2215
    {
2216
        if (is_object($value) || is_array($value)) {
2217
            /*
2218
            * Objects and arrays cannot be cast
2219
            * to a string without an error.
2220
            *
2221
            * unit test:
2222
            * _setDOMAttribute/throwXMLException.phpt
2223
            */
2224
            throw new XML_Query2XML_XMLException(
2225
                $configPath . ': A value of the type ' . gettype($value)
2226
                . ' cannot be used for an attribute value.'
2227
            );
2228
        }
2229
 
2230
        try {
2231
            $element->setAttribute($name, $value);
2232
        } catch(DOMException $e) {
2233
            // no unit test available for this one
2234
            throw new XML_Query2XML_XMLException(
2235
                $configPath . ': "' . $name . '" is an invalid XML attribute name: '
2236
                . $e->getMessage(),
2237
                $e
2238
            );
2239
        }
2240
    }
2241
 
2242
    /**
2243
     * Adds one or more child nodes to an existing DOMNode instance.
2244
     *
2245
     * @param DOMNode $base       An instance of DOMNode.
2246
     * @param mixed   $children   An array of DOMNode instances or
2247
     *                            just a single DOMNode instance.
2248
     *                            Boolean values of false are always ignored.
2249
     * @param string  $configPath The config path; used for exception messages.
2250
     * @param boolean $import     Whether DOMDocument::importNode() should be called
2251
     *                            for $children. This is necessary if the instance(s)
2252
     *                            passed as $children was/were created using a
2253
     *                            different DOMDocument instance. This argument is
2254
     *                            optional. The default is false.
2255
     *
2256
     * @return void
2257
     * @throws XML_Query2XML_XMLException If one of the specified children
2258
     *                         is not one of the following: an instance of DOMNode,
2259
     *                         the boolean value false, or an array containing
2260
     *                         these two.
2261
     */
2262
    private static function _addDOMChildren(DOMNode $base,
2263
                                            $children,
2264
                                            $configPath,
2265
                                            $import = false)
2266
    {
2267
        if ($children === false) {
2268
            // don't do anything
2269
            return;
2270
        } elseif ($children instanceof DOMNode) {
2271
            // $children is a single complex child
2272
            if ($import) {
2273
                $children = $base->ownerDocument->importNode($children, true);
2274
            }
2275
            $base->appendChild($children);
2276
        } elseif (is_array($children)) {
2277
            for ($i = 0; $i < count($children); $i++) {
2278
                if ($children[$i] === false) {
2279
                    // don't do anything
2280
                } elseif ($children[$i] instanceof DOMNode) {
2281
                    if ($import) {
2282
                        $children[$i] = $base->ownerDocument->importNode(
2283
                            $children[$i],
2284
                            true
2285
                        );
2286
                    }
2287
                    $base->appendChild($children[$i]);
2288
                } else {
2289
                    /*
2290
                    * unit tests:
2291
                    * _addDOMChildren/throwXMLException_arrayWithObject.phpt
2292
                    * _addDOMChildren/throwXMLException_arrayWithString.phpt
2293
                    * _addDOMChildren/throwXMLException_arrayWithInt.phpt
2294
                    * _addDOMChildren/throwXMLException_arrayWithBool.phpt
2295
                    * _addDOMChildren/throwXMLException_arrayWithDouble.phpt
2296
                    */
2297
                    throw new XML_Query2XML_XMLException(
2298
                        $configPath . ': DOMNode, false or an array of the two '
2299
                        . 'expected, but ' . gettype($children[$i]) . ' given '
2300
                        . '(hint: check your callback).'
2301
                    );
2302
                }
2303
            }
2304
        } else {
2305
            /*
2306
             * This should never happen because _addDOMChildren() is only called
2307
             * for arrays and instances of DOMNode.
2308
             */
2309
            throw new XML_Query2XML_XMLException(
2310
                $configPath . ': DOMNode, false or an array of the two '
2311
                . 'expected, but ' . gettype($children) . ' given '
2312
                . '(hint: check your callback).'
2313
            );
2314
        }
2315
    }
2316
 
2317
    /**
2318
     * Remove all container elements created by XML_Query2XML to ensure that all
2319
     * elements are correctly ordered.
2320
     *
2321
     * This is a recursive method. This method calls
2322
     * {@link XML_Query2XML::_replaceParentWithChildren()}. For the concept of
2323
     * container elements please see the {@tutorial XML_Query2XML.pkg tutorial}.
2324
     *
2325
     * @param DOMNode $element               An instance of DOMNode.
2326
     * @param string  $hiddenContainerPrefix The containers that will be removed
2327
     *                                       all start with this string.
2328
     *
2329
     * @return void
2330
     */
2331
    private static function _removeContainers($element, $hiddenContainerPrefix)
2332
    {
2333
        $xpath      = new DOMXPath($element);
2334
        $containers = $xpath->query(
2335
            '//*[starts-with(name(),\'' . $hiddenContainerPrefix . '\')]'
2336
        );
2337
        foreach ($containers as $container) {
2338
            if (!is_null($container->parentNode)) {
2339
                self::_replaceParentWithChildren($container);
2340
            }
2341
        }
2342
    }
2343
 
2344
    /**
2345
     * Replace a certain node with its child nodes.
2346
     *
2347
     * @param DOMNode $parent An instance of DOMNode.
2348
     *
2349
     * @return void
2350
     */
2351
    private static function _replaceParentWithChildren(DOMNode $parent)
2352
    {
2353
 
2354
        $child = $parent->firstChild;
2355
        while ($child) {
2356
            $nextChild = $child->nextSibling;
2357
            $parent->removeChild($child);
2358
            $parent->parentNode->insertBefore($child, $parent);
2359
            $child = $nextChild;
2360
        }
2361
        $parent->parentNode->removeChild($parent);
2362
    }
2363
 
2364
    /**
2365
     * Calls an encoder for XML node and attribute values
2366
     * $options['encoder'] can be one of the following:
2367
     * - null: self::_utf8encode() will be used
2368
     * - false: no encoding will be performed
2369
     * - callback: a string or an array as defined by the
2370
     *   callback pseudo-type; please see
2371
     *   http://www.php.net/manual/en/
2372
     *   language.pseudo-types.php#language.types.callback
2373
     *
2374
     * @param string $str     The string to encode
2375
     * @param array  $options An associative array with $options['encoder'] set.
2376
     *
2377
     * @return void
2378
     * @throws XML_Query2XML_XMLException If the $options['encoder'] is a callback
2379
     *                                    function that threw an exception.
2380
     */
2381
    private static function _executeEncoder($str, $options)
2382
    {
2383
        if (!is_string($str) || $options['encoder'] === false) {
2384
            return $str;
2385
        }
2386
 
2387
        if ($options['encoder'] === null) {
2388
            return self::_utf8encode($str);
2389
        }
2390
 
2391
        try {
2392
            return call_user_func($options['encoder'], $str);
2393
        } catch (Exception $e) {
2394
            /*
2395
            * unit test:
2396
            *  _executeEncoder/throwXMLException.phpt
2397
            */
2398
            throw new XML_Query2XML_XMLException(
2399
                $options['--q2x--path'] . '[encoder]: Could not encode '
2400
                . '"' . $str . '": ' . $e->getMessage()
2401
            );
2402
        }
2403
    }
2404
 
2405
    /**
2406
     * UTF-8 encode $str using mb_conver_encoding or if that is not
2407
     * present, utf8_encode.
2408
     *
2409
     * @param string $str The string to encode
2410
     *
2411
     * @return String The UTF-8 encoded version of $str
2412
     */
2413
    private static function _utf8encode($str)
2414
    {
2415
        if (function_exists('mb_convert_encoding')) {
2416
            $str = mb_convert_encoding($str, 'UTF-8');
2417
        } else {
2418
            $str = utf8_encode($str);
2419
        }
2420
        return $str;
2421
    }
2422
}
2423
 
2424
/**
2425
 * Parent class for ALL exceptions thrown by this package.
2426
 * By catching XML_Query2XML_Exception you will catch all exceptions
2427
 * thrown by XML_Query2XML.
2428
 *
2429
 * @category XML
2430
 * @package  XML_Query2XML
2431
 * @author   Lukas Feiler <lukas.feiler@lukasfeiler.com>
2432
 * @license  http://www.gnu.org/copyleft/lesser.html  LGPL Version 2.1
2433
 * @link     http://pear.php.net/package/XML_Query2XML
2434
 */
2435
class XML_Query2XML_Exception extends PEAR_Exception
2436
{
2437
 
2438
    /**
2439
     * Constructor method
2440
     *
2441
     * @param string    $message   The error message.
2442
     * @param Exception $exception The Exception that caused this exception
2443
     *                             to be thrown. This argument is optional.
2444
     */
2445
    public function __construct($message, $exception = null)
2446
    {
2447
        parent::__construct($message, $exception);
2448
    }
2449
}
2450
 
2451
/**
2452
 * Exception for driver errors
2453
 *
2454
 * @category XML
2455
 * @package  XML_Query2XML
2456
 * @author   Lukas Feiler <lukas.feiler@lukasfeiler.com>
2457
 * @license  http://www.gnu.org/copyleft/lesser.html  LGPL Version 2.1
2458
 * @link     http://pear.php.net/package/XML_Query2XML
2459
 * @since    Release 1.6.0RC1
2460
 */
2461
class XML_Query2XML_DriverException extends XML_Query2XML_Exception
2462
{
2463
    /**
2464
     * Constructor
2465
     *
2466
     * @param string $message The error message.
2467
     */
2468
    public function __construct($message)
2469
    {
2470
        parent::__construct($message);
2471
    }
2472
}
2473
 
2474
/**
2475
 * Exception for database errors
2476
 *
2477
 * @category XML
2478
 * @package  XML_Query2XML
2479
 * @author   Lukas Feiler <lukas.feiler@lukasfeiler.com>
2480
 * @license  http://www.gnu.org/copyleft/lesser.html  LGPL Version 2.1
2481
 * @link     http://pear.php.net/package/XML_Query2XML
2482
 */
2483
class XML_Query2XML_DBException extends XML_Query2XML_DriverException
2484
{
2485
    /**
2486
     * Constructor
2487
     *
2488
     * @param string $message The error message.
2489
     */
2490
    public function __construct($message)
2491
    {
2492
        parent::__construct($message);
2493
    }
2494
}
2495
 
2496
/**
2497
 * Exception for XML errors
2498
 * In most cases this exception will be thrown if a DOMException occurs.
2499
 *
2500
 * @category XML
2501
 * @package  XML_Query2XML
2502
 * @author   Lukas Feiler <lukas.feiler@lukasfeiler.com>
2503
 * @license  http://www.gnu.org/copyleft/lesser.html  LGPL Version 2.1
2504
 * @link     http://pear.php.net/package/XML_Query2XML
2505
 */
2506
class XML_Query2XML_XMLException extends XML_Query2XML_Exception
2507
{
2508
    /**
2509
     * Constructor
2510
     *
2511
     * @param string       $message   The error message.
2512
     * @param DOMException $exception The DOMException that caused this exception
2513
     *                                to be thrown. This argument is optional.
2514
     */
2515
    public function __construct($message, DOMException $exception = null)
2516
    {
2517
        parent::__construct($message, $exception);
2518
    }
2519
}
2520
 
2521
/**
2522
 * Exception that handles configuration errors.
2523
 *
2524
 * This exception handels errors in the $options array passed to
2525
 * XML_Query2XML::getXML() and wrong arguments passed to the constructor via
2526
 * XML_Query2XML::factory().
2527
 *
2528
 * @category XML
2529
 * @package  XML_Query2XML
2530
 * @author   Lukas Feiler <lukas.feiler@lukasfeiler.com>
2531
 * @license  http://www.gnu.org/copyleft/lesser.html  LGPL Version 2.1
2532
 * @link     http://pear.php.net/package/XML_Query2XML
2533
 * @see      XML_Query2XML::getXML()
2534
 */
2535
class XML_Query2XML_ConfigException extends XML_Query2XML_Exception
2536
{
2537
    /**
2538
     * Constructor method
2539
     *
2540
     * @param string $message A detailed error message.
2541
     */
2542
    public function __construct($message)
2543
    {
2544
        parent::__construct($message);
2545
    }
2546
}
2547
 
2548
/**
2549
 * Abstract driver class.
2550
 *
2551
 * usage:
2552
 * <code>
2553
 * $driver = XML_Query2XML_Driver::factory($backend);
2554
 * </code>
2555
 *
2556
 * @category XML
2557
 * @package  XML_Query2XML
2558
 * @author   Lukas Feiler <lukas.feiler@lukasfeiler.com>
2559
 * @license  http://www.gnu.org/copyleft/lesser.html  LGPL Version 2.1
2560
 * @version  Release: 1.7.2
2561
 * @link     http://pear.php.net/package/XML_Query2XML
2562
 * @since    Release 1.5.0RC1
2563
 */
2564
abstract class XML_Query2XML_Driver
2565
{
2566
    /**
2567
     * This method, when implemented executes the query passed as the
2568
     * first argument and returns all records from the result set.
2569
     *
2570
     * The format of the first argument depends on the driver being used.
2571
     *
2572
     * @param mixed  $sql        The SQL query as a string or an array.
2573
     * @param string $configPath The config path; used for exception messages.
2574
     *
2575
     * @return array An array of records. Each record itself will be an
2576
     *               associative array.
2577
     * @throws XML_Query2XML_DriverException If some driver related error occures.
2578
     */
2579
    abstract public function getAllRecords($sql, $configPath);
2580
 
2581
    /**
2582
     * Pre-processes a query specification and returns a string representation
2583
     * of the query.
2584
     *
2585
     * The returned string will be used for logging purposes. It
2586
     * does not need to be valid SQL.
2587
     *
2588
     * If $query is a string, it will be changed to array('query' => $query).
2589
     *
2590
     * @param mixed  &$query     A string or an array containing the element 'query'.
2591
     * @param string $configPath The config path; used for exception messages.
2592
     *
2593
     * @return string The query statement as a string.
2594
     * @throws XML_Query2XML_ConfigException If $query is an array but does not
2595
     *                                       contain the element 'query'.
2596
     */
2597
    public function preprocessQuery(&$query, $configPath)
2598
    {
2599
        if (is_string($query)) {
2600
            $query = array('query' => $query);
2601
        } elseif (is_array($query)) {
2602
            if (!isset($query['query'])) {
2603
                /*
2604
                * unit test: _preprocessOptions/
2605
                *  throwConfigException_queryOptionMissing.phpt
2606
                */
2607
                throw new XML_Query2XML_ConfigException(
2608
                    $configPath . ': The configuration option'
2609
                    . ' "query" is missing.'
2610
                );
2611
            }
2612
        } else { //neither a string nor an array
2613
            /*
2614
            * unit test: _preprocessOptions/
2615
            *  throwConfigException_sqlOptionWrongType.phpt
2616
            */
2617
            throw new XML_Query2XML_ConfigException(
2618
                $configPath . ': array or string expected, '
2619
                . gettype($query) . ' given.'
2620
            );
2621
        }
2622
        return $query['query'];
2623
    }
2624
 
2625
    /**
2626
     * Factory method.
2627
     *
2628
     * @param mixed $backend An instance of MDB2_Driver_Common, PDO, DB_common,
2629
     *                  ADOConnection, Net_LDAP2 or Net_LDAP.
2630
     *
2631
     * @return XML_Query2XML_Driver An instance of a driver class that
2632
     *                  extends XML_Query2XML_Driver.
2633
     * @throws XML_Query2XML_DriverException If $backend already is a PEAR_Error.
2634
     * @throws XML_Query2XML_ConfigException If $backend is not an instance of a
2635
     *                  child class of MDB2_Driver_Common, PDO, DB_common,
2636
     *                  ADOConnection, Net_LDAP2 or Net_LDAP.
2637
     */
2638
    public static function factory($backend)
2639
    {
2640
        if (
2641
            class_exists('MDB2_Driver_Common') &&
2642
            $backend instanceof MDB2_Driver_Common
2643
        ) {
2644
            include_once 'XML/Query2XML/Driver/MDB2.php';
2645
            return new XML_Query2XML_Driver_MDB2($backend);
2646
        } elseif (class_exists('PDO') && $backend instanceof PDO) {
2647
            include_once 'XML/Query2XML/Driver/PDO.php';
2648
            return new XML_Query2XML_Driver_PDO($backend);
2649
        } elseif (class_exists('DB_common') && $backend instanceof DB_common) {
2650
            include_once 'XML/Query2XML/Driver/DB.php';
2651
            return new XML_Query2XML_Driver_DB($backend);
2652
        } elseif (
2653
            class_exists('ADOConnection') &&
2654
            $backend instanceof ADOConnection
2655
        ) {
2656
            include_once 'XML/Query2XML/Driver/ADOdb.php';
2657
            return new XML_Query2XML_Driver_ADOdb($backend);
2658
        } elseif (class_exists('Net_LDAP') && $backend instanceof Net_LDAP) {
2659
            include_once 'XML/Query2XML/Driver/LDAP.php';
2660
            return new XML_Query2XML_Driver_LDAP($backend);
2661
        } elseif (class_exists('Net_LDAP2') && $backend instanceof Net_LDAP2) {
2662
            include_once 'XML/Query2XML/Driver/LDAP2.php';
2663
            return new XML_Query2XML_Driver_LDAP2($backend);
2664
        } elseif (class_exists('PEAR_Error') && $backend instanceof PEAR_Error) {
2665
            //unit tests: NoDBLayer/factory/throwDBException.phpt
2666
            throw new XML_Query2XML_DriverException(
2667
                'Driver error: ' . $backend->toString()
2668
            );
2669
        } else {
2670
            //unit test: NoDBLayer/factory/throwConfigException.phpt
2671
            throw new XML_Query2XML_ConfigException(
2672
                'Argument passed to the XML_Query2XML constructor is not an '
2673
                . 'instance of DB_common, MDB2_Driver_Common, ADOConnection'
2674
                . ', PDO, Net_LDAP or Net_LDAP2.'
2675
            );
2676
        }
2677
    }
2678
}
2679
?>