<?php

/** @file

 * This package contains classes for handling database transactions from

 * within QueryPath.

 *

 * The tools here use the PDO (PHP Data Objects) library to execute database

 * functions.

 *

 * Using tools in this package, you can write QueryPath database queries

 * that query an RDBMS and then insert the results into the document.

 *

 * Example:

 *

 * @code

 * <?php

 * $template = '<?xml version="1.0"?><tr><td class="colOne"/><td class="colTwo"/><td class="colThree"/></tr>';

 * $qp = qp(QueryPath::HTML_STUB, 'body') // Open a stub HTML doc and select <body/>

 *  ->append('<table><tbody/></table>')

 *  ->dbInit($this->dsn)

 *  ->queryInto('SELECT * FROM qpdb_test WHERE 1', array(), $template)

 *  ->doneWithQuery()

 *  ->writeHTML();

 * ?>

 * @endcode

 *

 * The code above will take the results of a SQL query and insert them into a n

 * HTML table.

 *

 * If you are doing many database operations across multiple QueryPath objects,

 * it is better to avoid using {@link QPDB::dbInit()}. Instead, you should 

 * call the static {@link QPDB::baseDB()} method to configure a single database

 * connection that can be shared by all {@link QueryPath} instances.

 *

 * Thus, we could rewrite the above to look like this:

 * @code

  * <?php

  * QPDB::baseDB($someDN);

  *

  * $template = '<?xml version="1.0"?><tr><td class="colOne"/><td class="colTwo"/><td class="colThree"/></tr>';

  * $qp = qp(QueryPath::HTML_STUB, 'body') // Open a stub HTML doc and select <body/>

  *  ->append('<table><tbody/></table>')

  *  ->queryInto('SELECT * FROM qpdb_test WHERE 1', array(), $template)

  *  ->doneWithQuery()

  *  ->writeHTML();

  * ?>

  * @endcode

 *

 * Note that in this case, the QueryPath object doesn't need to call a method to

 * activate the database. There is no call to {@link dbInit()}. Instead, it checks

 * the base class to find the shared database.

 *

 * (Note that if you were to add a dbInit() call to the above, it would create

 * a new database connection.)

 *

 * The result of both of these examples will be identical.

 * The output looks something like this:

 * 

 * @code

 * <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

 * <html xmlns="http://www.w3.org/1999/xhtml">

 * <head>

 *  <meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta>

 * 	<title>Untitled</title>

 * </head>

 *<body>

 *<table>

 * <tbody>

 *  <tr>

 *   <td class="colOne">Title 0</td>

 *   <td class="colTwo">Body 0</td>

 *   <td class="colThree">Footer 0</td>

 *  </tr>

 *  <tr>

 *   <td class="colOne">Title 1</td>

 *   <td class="colTwo">Body 1</td>

 *   <td class="colThree">Footer 1</td>

 *  </tr>

 *  <tr>

 *   <td class="colOne">Title 2</td>

 *   <td class="colTwo">Body 2</td>

 *   <td class="colThree">Footer 2</td>

 *  </tr>

 *  <tr>

 *   <td class="colOne">Title 3</td>

 *   <td class="colTwo">Body 3</td>

 *   <td class="colThree">Footer 3</td>

 *  </tr>

 *  <tr>

 *   <td class="colOne">Title 4</td>

 *   <td class="colTwo">Body 4</td>

 *   <td class="colThree">Footer 4</td>

 *  </tr>

 * </tbody>

 *</table>

 *</body>

 *</html>

 * @endcode

 *

 * Note how the CSS classes are used to correlate DB table names to template

 * locations.

 *

 * 

 * @author M Butcher <matt@aleph-null.tv>

 * @license http://opensource.org/licenses/lgpl-2.1.php LGPL or MIT-like license.

 * @see QueryPathExtension

 * @see QueryPathExtensionRegistry::extend()

 * @see QPDB

 */

 

/**

 * Provide DB access to a QueryPath object.

 *

 * This extension provides tools for communicating with a database using the 

 * QueryPath library. It relies upon PDO for underlying database communiction. This

 * means that it supports all databases that PDO supports, including MySQL, 

 * PostgreSQL, and SQLite.

 *

 * Here is an extended example taken from the unit tests for this library.

 * 

 * Let's say we create a database with code like this:

 * @code

 *<?php

 * public function setUp() {

 *   $this->db = new PDO($this->dsn);

 *   $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

 *   $this->db->exec('CREATE TABLE IF NOT EXISTS qpdb_test (colOne, colTwo, colThree)');

 *   

 *   $stmt = $this->db->prepare(

 *     'INSERT INTO qpdb_test (colOne, colTwo, colThree) VALUES (:one, :two, :three)'

 *   );

 *   

 *   for ($i = 0; $i < 5; ++$i) {

 *     $vals = array(':one' => 'Title ' . $i, ':two' => 'Body ' . $i, ':three' => 'Footer ' . $i);

 *     $stmt->execute($vals);

 *     $stmt->closeCursor();

 *   }

 * }

 * ?>

 * @endcode

 * 

 * From QueryPath with QPDB, we can now do very elaborate DB chains like this:

 *

 * @code

 * <?php

 * $sql = 'SELECT * FROM qpdb_test';

 * $args = array();

 * $qp = qp(QueryPath::HTML_STUB, 'body') // Open a stub HTML doc and select <body/>

 *   ->append('<h1></h1>') // Add <h1/>

 *   ->children()  // Select the <h1/>

 *   ->dbInit($this->dsn) // Connect to the database

 *   ->query($sql, $args) // Execute the SQL query

 *   ->nextRow()  // Select a row. By default, no row is selected.

 *   ->appendColumn('colOne') // Append Row 1, Col 1 (Title 0)

 *   ->parent() // Go back to the <body/>

 *   ->append('<p/>') // Append a <p/> to the body

 *   ->find('p')  // Find the <p/> we just created.

 *   ->nextRow() // Advance to row 2

 *   ->prependColumn('colTwo') // Get row 2, col 2. (Body 1)

 *   ->columnAfter('colThree') // Get row 2 col 3. (Footer 1)

 *   ->doneWithQuery() // Let QueryPath clean up. YOU SHOULD ALWAYS DO THIS.

 *   ->writeHTML(); // Write the output as HTML.

 * ?> 

 * @endcode

 *

 * With the code above, we step through the document, selectively building elements

 * as we go, and then populating this elements with data from our initial query.

 *

 * When the last command, {@link QueryPath:::writeHTML()}, is run, we will get output

 * like this:

 * 

 * @code

 *   <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

 *   <html xmlns="http://www.w3.org/1999/xhtml">

 *     <head>

 *     	<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

 *     	<title>Untitled</title>

 *     </head>

 *     <body>

 *       <h1>Title 0</h1>

 *       <p>Body 1</p>

 *       Footer 1</body>

 *    </html>

 * @endcode

 *

 * Notice the body section in particular. This is where the data has been

 * inserted.

 *

 * Sometimes you want to do something a lot simpler, like give QueryPath a 

 * template and have it navigate a query, inserting the data into a template, and

 * then inserting the template into the document. This can be done simply with 

 * the {@link queryInto()} function.

 *

 * Here's an example from another unit test:

 *

 * @code

 * <?php

 * $template = '<?xml version="1.0"?><li class="colOne"/>';

 * $sql = 'SELECT * FROM qpdb_test';

 * $args = array();

 * $qp = qp(QueryPath::HTML_STUB, 'body')

 *   ->append('<ul/>') // Add a new <ul/>

 *   ->children() // Select the <ul/>

 *   ->dbInit($this->dsn) // Initialize the DB

 *   // BIG LINE: Query the results, run them through the template, and insert them.

 *   ->queryInto($sql, $args, $template) 

 *   ->doneWithQuery()

 *   ->writeHTML(); // Write the results as HTML.

 * ?>

 * @endcode 

 *

 * The simple code above puts the first column of the select statement

 * into an unordered list. The example output looks like this:

 *

 * @code

 * <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

 * <html xmlns="http://www.w3.org/1999/xhtml">

 *   <head>

 *   	<meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta>

 *   	<title>Untitled</title>

 *    </head>

 *    <body>

 *    <ul>

 *    <li class="colOne">Title 0</li>

 *    <li class="colOne">Title 1</li>

 *    <li class="colOne">Title 2</li>

 *    <li class="colOne">Title 3</li>

 *    <li class="colOne">Title 4</li>

 *    </ul>

 *   </body>

 * </html>

 * @endcode

 *

 * Typical starting methods for this class are {@link QPDB::baseDB()}, 

 * {@link QPDB::query()}, and {@link QPDB::queryInto()}.

 *

 * @ingroup querypath_extensions

 */

class QPDB implements QueryPathExtension {

  protected $qp;

  protected $dsn;

  protected $db;

  protected $opts;

  protected $row = NULL;

  protected $stmt = NULL;

  

  protected static $con = NULL;

  

  /**

   * Create a new database instance for all QueryPath objects to share.

   *

   * This method need be called only once. From there, other QPDB instances

   * will (by default) share the same database instance.

   *

   * Normally, a DSN should be passed in. Username, password, and db params

   * are all passed in using the options array.

   *

   * On rare occasions, it may be more fitting to pass in an existing database

   * connection (which must be a {@link PDO} object). In such cases, the $dsn

   * parameter can take a PDO object instead of a DSN string. The standard options

   * will be ignored, though.

   *

   * <b>Warning:</b> If you pass in a PDO object that is configured to NOT throw

   * exceptions, you will need to handle error checking differently.

   *

   * <b>Remember to always use {@link QPDB::doneWithQuery()} when you are done

   * with a query. It gives PDO a chance to clean up open connections that may

   * prevent other instances from accessing or modifying data.</b>

   *

   * @param string $dsn

   *  The DSN of the database to connect to. You can also pass in a PDO object, which

   *  will set the QPDB object's database to the one passed in.

   * @param array $options

   *  An array of configuration options. The following options are currently supported:

   *  - username => (string)

   *  - password => (string)

   *  - db params => (array) These will be passed into the new PDO object.

   *    See the PDO documentation for a list of options. By default, the

   *    only flag set is {@link PDO::ATTR_ERRMODE}, which is set to 

   *    {@link PDO::ERRMODE_EXCEPTION}.

   * @throws PDOException

   *  An exception may be thrown if the connection cannot be made.

   */

  static function baseDB($dsn, $options = array()) {

    

    $opts = $options + array(

      'username' => NULL,

      'password' => NULL,

      'db params' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),

    );

    

    // Allow this to handle the case where an outside

    // connection does the initialization.

    if ($dsn instanceof PDO) {

      self::$con = $dsn;

      return;

    }

    self::$con = new PDO($dsn, $opts['username'], $opts['password'], $opts['db params']);

  }

  

  /**

   * 

   * This method may be used to share the connection with other, 

   * non-QueryPath objects.

   */

  static function getBaseDB() {return self::$con;}

  

  /**

   * Used to control whether or not all rows in a result should be cycled through.

   */

  protected $cycleRows = FALSE;

  

  /**

   * Construct a new QPDB object. This is usually done by QueryPath itself.

   */

  public function __construct(QueryPath $qp) {

    $this->qp = $qp;

    // By default, we set it up to use the base DB.

    $this->db = self::$con;

  }

  

  /**

   * Create a new connection to the database. Use the PDO DSN syntax for a 

   * connection string.

   *

   * This creates a database connection that will last for the duration of 

   * the QueryPath object. This method ought to be used only in two cases:

   * - When you will only run a couple of queries during the life of the 

   *   process.

   * - When you need to connect to a database that will only be used for 

   *   a few things.

   * Otherwise, you should use {@link QPDB::baseDB} to configure a single

   * database connection that all of {@link QueryPath} can share.

   *

   * <b>Remember to always use {@link QPDB::doneWithQuery()} when you are done

   * with a query. It gives PDO a chance to clean up open connections that may

   * prevent other instances from accessing or modifying data.</b>

   *

   * @param string $dsn

   *  The PDO DSN connection string.

   * @param array $options

   *  Connection options. The following options are supported:

   *  - username => (string)

   *  - password => (string)

   *  - db params => (array) These will be passed into the new PDO object.

   *    See the PDO documentation for a list of options. By default, the

   *    only flag set is {@link PDO::ATTR_ERRMODE}, which is set to 

   *    {@link PDO::ERRMODE_EXCEPTION}.

   * @return QueryPath

   *  The QueryPath object.

   * @throws PDOException

   *  The PDO library is configured to throw exceptions, so any of the 

   *  database functions may throw a PDOException.

   */

  public function dbInit($dsn, $options = array()) {

    $this->opts = $options + array(

      'username' => NULL,

      'password' => NULL,

      'db params' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),

    );

    $this->dsn = $dsn;

    $this->db = new PDO($dsn, $this->opts['username'], $this->opts['password'], $this->opts['db params']);

    /*

    foreach ($this->opts['db params'] as $key => $val)

      $this->db->setAttribute($key, $val);

    */

    

    return $this->qp;

  }

  

  /**

   * Execute a SQL query, and store the results.

   *

   * This will execute a SQL query (as a prepared statement), and then store

   * the results internally for later use. The data can be iterated using 

   * {@link nextRow()}. QueryPath can also be instructed to do internal iteration

   * using the {@link withEachRow()} method. Finally, on the occasion that the 

   * statement itself is needed, {@link getStatement()} can be used.

   *

   * Use this when you need to access the results of a query, or when the 

   * parameter to a query should be escaped. If the query takes no external 

   * parameters and does not return results, you may wish to use the 

   * (ever so slightly faster) {@link exec()} function instead.

   * 

   * Make sure you use {@link doneWithQuery()} after finishing with the database

   * results returned by this method.

   *

   * <b>Usage</b>

   * 

   * Here is a simple example:

   * <code>

   * <?php

   * QPQDB::baseDB($someDSN);

   * 

   * $args = array(':something' => 'myColumn');

   * qp()->query('SELECT :something FROM foo', $args)->doneWithQuery();

   * ?>

   * </code>

   *

   * The above would execute the given query, substituting myColumn in place of 

   * :something before executing the query The {@link doneWithQuery()} method

   * indicates that we are not going to use the results for anything. This method

   * discards the results.

   *

   * A more typical use of the query() function would involve inserting data

   * using {@link appendColumn()}, {@link prependColumn()}, {@link columnBefore()},

   * or {@link columnAfter()}. See the main documentation for {@link QPDB} to view

   * a more realistic example.

   * 

   * @param string $sql

   *  The query to be executed.

   * @param array $args

   *  An associative array of substitutions to make.

   * @throws PDOException

   *  Throws an exception if the query cannot be executed.

   */

  public function query($sql, $args = array()) {

    $this->stmt = $this->db->prepare($sql);

    $this->stmt->execute($args);

    return $this->qp;

  }

  

  /**

   * Query and append the results.

   *

   * Run a query and inject the results directly into the 

   * elements in the QueryPath object.

   *

   * If the third argument is empty, the data will be inserted directly into

   * the QueryPath elements unaltered. However, if a template is provided in 

   * the third parameter, the query data will be merged into that template

   * and then be added to each QueryPath element.

   *

   * The template will be merged once for each row, even if no row data is

   * appended into the template.

   *

   * A template is simply a piece of markup labeled for insertion of

   * data. See {@link QPTPL} and {@link QPTPL.php} for more information.

   *

   * Since this does not use a stanard {@link query()}, there is no need

   * to call {@link doneWithQuery()} after this method.

   *

   * @param string $sql

   *  The SQL query to execute. In this context, the query is typically a 

   *  SELECT statement.

   * @param array $args

   *  An array of arguments to be substituted into the query. See {@link query()}

   *  for details.

   * @param mixed $template

   *  A template into which query results will be merged prior to being appended

   *  into the QueryPath. For details on the template, see {@link QPTPL::tpl()}.

   * @see QPTPL.php

   * @see QPTPL::tpl()

   * @see query()

   */

  public function queryInto($sql, $args = array(), $template = NULL) {

    $stmt = $this->db->prepare($sql);

    $stmt->setFetchMode(PDO::FETCH_ASSOC);

    $stmt->execute($args);

    

    // If no template, put all values in together.

    if (empty($template)) {

      foreach ($stmt as $row) foreach ($row as $datum) $this->qp->append($datum);

    }

    // Otherwise, we run the results through a template, and then append.

    else {

      foreach ($stmt as $row) $this->qp->tpl($template, $row);

    }

    

    $stmt->closeCursor();

    return $this->qp;

  }

  

  /**

   * Free up resources when a query is no longer used.

   *

   * This function should <i>always</i> be called when the database

   * results for a query are no longer needed. This frees up the 

   * database cursor, discards the data, and resets resources for future

   * use. 

   *

   * If this method is not called, some PDO database drivers will not allow

   * subsequent queries, while others will keep tables in a locked state where

   * writes will not be allowed.

   *

   * @return QueryPath

   *  The QueryPath object.

   */

  public function doneWithQuery() {

    if (isset($this->stmt) && $this->stmt instanceof PDOStatement) {

      // Some drivers choke if results haven't been iterated.

      //while($this->stmt->fetch()) {}

      $this->stmt->closeCursor();

    }

      

    unset($this->stmt);

    $this->row = NULL;

    $this->cycleRows = FALSE;

    return $this->qp;

  }

  

  /**

   * Execute a SQL query, but expect no value.

   * 

   * If your SQL query will have parameters, you are encouraged to

   * use {@link query()}, which includes built-in SQL Injection 

   * protection.

   *

   * @param string $sql

   *  A SQL statement.

   * @throws PDOException 

   *  An exception will be thrown if a query cannot be executed.

   */

  public function exec($sql) {

    $this->db->exec($sql);

    return $this->qp;

  }

  

  /**

   * Advance the query results row cursor.

   *

   * In a result set where more than one row was returned, this will 

   * move the pointer to the next row in the set.

   *

   * The PDO library does not have a consistent way of determining how many

   * rows a result set has. The suggested technique is to first execute a

   * COUNT() SQL query and get the data from that.

   *

   * The {@link withEachRow()} method will begin at the next row after the

   * currently selected one.

   *

   * @return QueryPath

   *  The QueryPath object.

   */

  public function nextRow() {

    $this->row = $this->stmt->fetch(PDO::FETCH_ASSOC);

    return $this->qp;

  }

  

  /**

   * Set the object to use each row, instead of only one row.

   *

   * This is used primarily to instruct QPDB to iterate through all of the 

   * rows when appending, prepending, inserting before, or inserting after.

   *

   * @return QueryPath

   *  The QueryPath object.

   * @see appendColumn()

   * @see prependColumn()

   * @see columnBefore()

   * @see columnAfter()

   */

  public function withEachRow() {

    $this->cycleRows = TRUE;

    return $this->qp;

  }

  

  /**

   * This is the implementation behind the append/prepend and before/after methods.

   *

   * @param mixed $columnName

   *  The name of the column whose data should be added to the currently selected

   *  elements. This can be either a string or an array of strings.

   * @param string $qpFunc

   *  The name of the QueryPath function that should be executed to insert data

   *  into the object.

   * @param string $wrap

   *  The HTML/XML markup that will be used to wrap around the column data before

   *  the data is inserted into the QueryPath object.

   */

  protected function addData($columnName, $qpFunc = 'append', $wrap = NULL) {

    $columns = is_array($columnName) ? $columnName : array($columnName);

    $hasWrap = !empty($wrap);

    if ($this->cycleRows) {

      while (($row = $this->stmt->fetch(PDO::FETCH_ASSOC)) !== FALSE) {

        foreach ($columns as $col) {

          if (isset($row[$col])) {

            $data = $row[$col];

            if ($hasWrap) 

              $data = qp()->append($wrap)->deepest()->append($data)->top();

            $this->qp->$qpFunc($data);

          }

        }

      }

      $this->cycleRows = FALSE;

      $this->doneWithQuery();

    }

    else {

      if ($this->row !== FALSE) {

        foreach ($columns as $col) {

          if (isset($this->row[$col])) {

            $data = $this->row[$col];

            if ($hasWrap) 

              $data = qp()->append($wrap)->deepest()->append($data)->top();

            $this->qp->$qpFunc($data);

          }

        }

      }

    }

    return $this->qp;

  }

  

  /**

   * Get back the raw PDOStatement object after a {@link query()}.

   *

   * @return PDOStatement

   *  Return the PDO statement object. If this is called and no statement

   *  has been executed (or the statement has already been cleaned up),

   *  this will return NULL.

   */

  public function getStatement() {

    return $this->stmt;

  }

  

  /**

   * Get the last insert ID.

   *

   * This will only return a meaningful result when used after an INSERT.

   *

   * @return mixed

   *  Return the ID from the last insert. The value and behavior of this

   *  is database-dependent. See the official PDO driver documentation for

   *  the database you are using.

   * @since 1.3

   */

  public function getLastInsertID() {

    $con = self::$con;

    return $con->lastInsertId();

  }

  

  /**

   * Append the data in the given column(s) to the QueryPath.

   *

   * This appends data to every item in the current QueryPath. The data will

   * be retrieved from the database result, using $columnName as the key.

   *

   * @param mixed $columnName

   *  Either a string or an array of strings. The value(s) here should match 

   *  one or more column headers from the current SQL {@link query}'s results.

   * @param string $wrap

   *  IF this is supplied, then the value or values retrieved from the database

   *  will be wrapped in this HTML/XML before being inserted into the QueryPath.

   * @see QueryPath::wrap()

   * @see QueryPath::append()

   */

  public function appendColumn($columnName, $wrap = NULL) {

    return $this->addData($columnName, 'append', $wrap); 

  }

  

  /**

   * Prepend the data from the given column into the QueryPath.

   *

   * This takes the data from the given column(s) and inserts it into each

   * element currently found in the QueryPath.

   * @param mixed $columnName

   *  Either a string or an array of strings. The value(s) here should match 

   *  one or more column headers from the current SQL {@link query}'s results.

   * @param string $wrap

   *  IF this is supplied, then the value or values retrieved from the database

   *  will be wrapped in this HTML/XML before being inserted into the QueryPath.

   * @see QueryPath::wrap()

   * @see QueryPath::prepend()

   */

  public function prependColumn($columnName, $wrap = NULL) {

    return $this->addData($columnName, 'prepend', $wrap);

  }

  

  /**

   * Insert the data from the given column before each element in the QueryPath.

   *

   * This inserts the data before each element in the currently matched QueryPath.

   *

   * @param mixed $columnName

   *  Either a string or an array of strings. The value(s) here should match 

   *  one or more column headers from the current SQL {@link query}'s results.

   * @param string $wrap

   *  IF this is supplied, then the value or values retrieved from the database

   *  will be wrapped in this HTML/XML before being inserted into the QueryPath.

   * @see QueryPath::wrap()

   * @see QueryPath::before()

   * @see prependColumn()

   */

  public function columnBefore($columnName, $wrap = NULL) {

    return $this->addData($columnName, 'before', $wrap);

  }

  

  /**

   * Insert data from the given column(s) after each element in the QueryPath.

   *

   * This inserts data from the given columns after each element in the QueryPath

   * object. IF HTML/XML is given in the $wrap parameter, then the column data

   * will be wrapped in that markup before being inserted into the QueryPath.

   *

   * @param mixed $columnName

   *  Either a string or an array of strings. The value(s) here should match 

   *  one or more column headers from the current SQL {@link query}'s results.

   * @param string $wrap

   *  IF this is supplied, then the value or values retrieved from the database

   *  will be wrapped in this HTML/XML before being inserted into the QueryPath.

   * @see QueryPath::wrap()

   * @see QueryPath::after()

   * @see appendColumn()

   */

  public function columnAfter($columnName, $wrap = NULL) {

    return $this->addData($columnName, 'after', $wrap);

  }

  

}



// The define allows another class to extend this.

if (!defined('QPDB_OVERRIDE'))

  QueryPathExtensionRegistry::extend('QPDB');