diff options
Diffstat (limited to 'lib/querypath/Extension')
-rw-r--r-- | lib/querypath/Extension/QPDB.php | 711 | ||||
-rw-r--r-- | lib/querypath/Extension/QPList.php | 213 | ||||
-rw-r--r-- | lib/querypath/Extension/QPTPL.php | 275 | ||||
-rw-r--r-- | lib/querypath/Extension/QPXML.php | 209 | ||||
-rw-r--r-- | lib/querypath/Extension/QPXSL.php | 75 |
5 files changed, 1483 insertions, 0 deletions
diff --git a/lib/querypath/Extension/QPDB.php b/lib/querypath/Extension/QPDB.php new file mode 100644 index 0000000..1a41657 --- /dev/null +++ b/lib/querypath/Extension/QPDB.php @@ -0,0 +1,711 @@ +<?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');
\ No newline at end of file diff --git a/lib/querypath/Extension/QPList.php b/lib/querypath/Extension/QPList.php new file mode 100644 index 0000000..87fb860 --- /dev/null +++ b/lib/querypath/Extension/QPList.php @@ -0,0 +1,213 @@ +<?php +/** @file + * This extension provides support for common HTML list operations. + */ + +/** + * Provide list operations for QueryPath. + * + * The QPList class is an extension to QueryPath. It provides HTML list generators + * that take lists and convert them into bulleted lists inside of QueryPath. + * + * @deprecated This will be removed from a subsequent version of QueryPath. It will + * be released as a stand-alone extension. + * @ingroup querypath_extensions + */ +class QPList implements QueryPathExtension { + const UL = 'ul'; + const OL = 'ol'; + const DL = 'dl'; + + protected $qp = NULL; + public function __construct(QueryPath $qp) { + $this->qp = $qp; + } + + public function appendTable($items, $options = array()) { + $opts = $options + array( + 'table class' => 'qptable', + ); + $base = '<?xml version="1.0"?> + <table> + <tbody> + <tr></tr> + </tbody> + </table>'; + + $qp = qp($base, 'table')->addClass($opts['table class'])->find('tr'); + if ($items instanceof TableAble) { + $headers = $items->getHeaders(); + $rows = $items->getRows(); + } + elseif ($items instanceof Traversable) { + $headers = array(); + $rows = $items; + } + else { + $headers = $items['headers']; + $rows = $items['rows']; + } + + // Add Headers: + foreach ($headers as $header) { + $qp->append('<th>' . $header . '</th>'); + } + $qp->top()->find('tr:last'); + + // Add rows and cells. + foreach ($rows as $row) { + $qp->after('<tr/>')->next(); + foreach($row as $cell) $qp->append('<td>' . $cell . '</td>'); + } + + $this->qp->append($qp->top()); + + return $this->qp; + } + + /** + * Append a list of items into an HTML DOM using one of the HTML list structures. + * This takes a one-dimensional array and converts it into an HTML UL or OL list, + * <b>or</b> it can take an associative array and convert that into a DL list. + * + * In addition to arrays, this works with any Traversable or Iterator object. + * + * OL/UL arrays can be nested. + * + * @param mixed $items + * An indexed array for UL and OL, or an associative array for DL. Iterator and + * Traversable objects can also be used. + * @param string $type + * One of ul, ol, or dl. Predefined constants are available for use. + * @param array $options + * An associative array of configuration options. The supported options are: + * - 'list class': The class that will be assigned to a list. + */ + public function appendList($items, $type = self::UL, $options = array()) { + $opts = $options + array( + 'list class' => 'qplist', + ); + if ($type == self::DL) { + $q = qp('<?xml version="1.0"?><dl></dl>', 'dl')->addClass($opts['list class']); + foreach ($items as $dt => $dd) { + $q->append('<dt>' . $dt . '</dt><dd>' . $dd . '</dd>'); + } + $q->appendTo($this->qp); + } + else { + $q = $this->listImpl($items, $type, $opts); + $this->qp->append($q->find(':root')); + } + + return $this->qp; + } + + /** + * Internal recursive list generator for appendList. + */ + protected function listImpl($items, $type, $opts, $q = NULL) { + $ele = '<' . $type . '/>'; + if (!isset($q)) + $q = qp()->append($ele)->addClass($opts['list class']); + + foreach ($items as $li) { + if ($li instanceof QueryPath) { + $q = $this->listImpl($li->get(), $type, $opts, $q); + } + elseif (is_array($li) || $li instanceof Traversable) { + $q->append('<li><ul/></li>')->find('li:last > ul'); + $q = $this->listImpl($li, $type, $opts, $q); + $q->parent(); + } + else { + $q->append('<li>' . $li . '</li>'); + } + } + return $q; + } + + /** + * Unused. + */ + protected function isAssoc($array) { + // A clever method from comment on is_array() doc page: + return count(array_diff_key($array, range(0, count($array) - 1))) != 0; + } +} +QueryPathExtensionRegistry::extend('QPList'); + +/** + * A TableAble object represents tabular data and can be converted to a table. + * + * The {@link QPList} extension to {@link QueryPath} provides a method for + * appending a table to a DOM ({@link QPList::appendTable()}). + * + * Implementing classes should provide methods for getting headers, rows + * of data, and the number of rows in the table ({@link TableAble::size()}). + * Implementors may also choose to make classes Iterable or Traversable over + * the rows of the table. + * + * Two very basic implementations of TableAble are provided in this package: + * - {@link QPTableData} provides a generic implementation. + * - {@link QPTableTextData} provides a generic implementation that also escapes + * all data. + */ +interface TableAble { + public function getHeaders(); + public function getRows(); + public function size(); +} + +/** + * Format data to be inserted into a simple HTML table. + * + * Data in the headers or rows may contain markup. If you want to + * disallow markup, use a {@see QPTableTextData} object instead. + */ +class QPTableData implements TableAble, IteratorAggregate { + + protected $headers; + protected $rows; + protected $caption; + protected $p = -1; + + public function setHeaders($array) {$this->headers = $array; return $this;} + public function getHeaders() {return $this->headers; } + public function setRows($array) {$this->rows = $array; return $this;} + public function getRows() {return $this->rows;} + public function size() {return count($this->rows);} + public function getIterator() { + return new ArrayIterator($rows); + } +} + +/** + * Provides a table where all of the headers and data are treated as text data. + * + * This provents marked-up data from being inserted into the DOM as elements. + * Instead, the text is escaped using {@see htmlentities()}. + * + * @see QPTableData + */ +class QPTableTextData extends QPTableData { + public function setHeaders($array) { + $headers = array(); + foreach ($array as $header) { + $headers[] = htmlentities($header); + } + parent::setHeaders($headers); + return $this; + } + public function setRows($array) { + $count = count($array); + for ($i = 0; $i < $count; ++$i) { + $cols = array(); + foreach ($data[$i] as $datum) { + $cols[] = htmlentities($datum); + } + $data[$i] = $cols; + } + parent::setRows($array); + return $this; + } +}
\ No newline at end of file diff --git a/lib/querypath/Extension/QPTPL.php b/lib/querypath/Extension/QPTPL.php new file mode 100644 index 0000000..6e47935 --- /dev/null +++ b/lib/querypath/Extension/QPTPL.php @@ -0,0 +1,275 @@ +<?php +/** @file + * QueryPath templates. See QPTPL. + */ +/** + * QPTPL is a template library for QueryPath. + * + * The QPTPL extension provides template tools that can be used in + * conjunction with QueryPath. + * + * There are two basic modes in which this tool operates. Both merge data into + * a pure HTML template. Both base their insertions on classes and IDs in the + * HTML data. Where they differ is in the kind of data merged into the template. + * + * One mode takes array data and does a deep (recursive) merge into the template. + * It can be used for simple substitutions, but it can also be used to loop through + * "rows" of data and create tables. + * + * The second mode takes a classed object and introspects that object to find out + * what CSS classes it is capable of filling. This is one way of bridging an object + * model and QueryPath data. + * + * The unit tests are a good place for documentation, as is the QueryPath webste. + * + * @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 https://fedorahosted.org/querypath/wiki/QueryPathTemplate + * @ingroup querypath_extensions + */ +class QPTPL implements QueryPathExtension { + protected $qp; + public function __construct(QueryPath $qp) { + $this->qp = $qp; + } + + /** + * Apply a template to an object and then insert the results. + * + * This takes a template (an arbitrary fragment of XML/HTML) and an object + * or array and inserts the contents of the object into the template. The + * template is then appended to all of the nodes in the current list. + * + * Note that the data in the object is *not* escaped before it is merged + * into the template. For that reason, an object can return markup (as + * long as it is well-formed). + * + * @param mixed $template + * The template. It can be of any of the types that {@link qp()} supports + * natively. Typically it is a string of XML/HTML. + * @param mixed $object + * Either an object or an associative array. + * - In the case where the parameter + * is an object, this will introspect the object, looking for getters (a la + * Java bean behavior). It will then search the document for CSS classes + * that match the method name. The function is then executed and its contents + * inserted into the document. (If the function returns NULL, nothing is + * inserted.) + * - In the case where the paramter is an associative array, the function will + * look through the template for CSS classes that match the keys of the + * array. When an array key is found, the array value is inserted into the + * DOM as a child of the currently matched element(s). + * @param array $options + * The options for this function. Valid options are: + * - <None defined yet> + * @return QueryPath + * Returns a QueryPath object with all of the changes from the template + * applied into the QueryPath elements. + * @see QueryPath::append() + */ + public function tpl($template, $object, $options = array()) { + // Handle default options here. + + //$tqp = ($template instanceof QueryPath) ? clone $template: qp($template); + $tqp = qp($template); + + if (is_array($object) || $object instanceof Traversable) { + $this->tplArrayR($tqp, $object, $options); + return $this->qp->append($tqp->top()); + } + elseif (is_object($object)) { + $this->tplObject($tqp, $object, $options); + } + + return $this->qp->append($tqp->top()); + } + + /** + * Given one template, do substitutions for all objects. + * + * Using this method, one template can be populated from a variety of + * sources. That one template is then appended to the QueryPath object. + * @see tpl() + * @param mixed $template + * The template. It can be of any of the types that {@link qp()} supports + * natively. Typically it is a string of XML/HTML. + * @param array $objects + * An indexed array containing a list of objects or arrays (See {@link tpl()}) + * that will be merged into the template. + * @param array $options + * An array of options. See {@link tpl()} for a list. + * @return QueryPath + * Returns the QueryPath object. + */ + public function tplAll($template, $objects, $options = array()) { + $tqp = qp($template, ':root'); + foreach ($objects as $object) { + if (is_array($object)) + $tqp = $this->tplArrayR($tqp, $object, $options); + elseif (is_object($object)) + $tqp = $this->tplObject($tqp, $object, $options); + } + return $this->qp->append($tqp->top()); + } + + /* + protected function tplArray($tqp, $array, $options = array()) { + + // If we find something that's not an array, we try to handle it. + if (!is_array($array)) { + is_object($array) ? $this->tplObject($tqp, $array, $options) : $tqp->append($array); + } + // An assoc array means we have mappings of classes to content. + elseif ($this->isAssoc($array)) { + print 'Assoc array found.' . PHP_EOL; + foreach ($array as $key => $value) { + $first = substr($key,0,1); + + // We allow classes and IDs if explicit. Otherwise we assume + // a class. + if ($first != '.' && $first != '#') $key = '.' . $key; + + if ($tqp->top()->find($key)->size() > 0) { + print "Value: " . $value . PHP_EOL; + if (is_array($value)) { + //$newqp = qp($tqp)->cloneAll(); + print $tqp->xml(); + $this->tplArray($tqp, $value, $options); + print "Finished recursion\n"; + } + else { + print 'QP is ' . $tqp->size() . " inserting value: " . $value . PHP_EOL; + + $tqp->append($value); + } + } + } + } + // An indexed array means we have multiple instances of class->content matches. + // We copy the portion of the template and then call repeatedly. + else { + print "Array of arrays found..\n"; + foreach ($array as $array2) { + $clone = qp($tqp->xml()); + $this->tplArray($clone, $array2, $options); + print "Now appending clone.\n" . $clone->xml(); + $tqp->append($clone->parent()); + } + } + + + //return $tqp->top(); + return $tqp; + } + */ + + /** + * Introspect objects to map their functions to CSS classes in a template. + */ + protected function tplObject($tqp, $object, $options = array()) { + $ref = new ReflectionObject($object); + $methods = $ref->getMethods(); + foreach ($methods as $method) { + if (strpos($method->getName(), 'get') === 0) { + $cssClass = $this->method2class($method->getName()); + if ($tqp->top()->find($cssClass)->size() > 0) { + $tqp->append($method->invoke($object)); + } + else { + // Revert to the find() that found something. + $tqp->end(); + } + } + } + //return $tqp->top(); + return $tqp; + } + + /** + * Recursively merge array data into a template. + */ + public function tplArrayR($qp, $array, $options = NULL) { + // If the value looks primitive, append it. + if (!is_array($array) && !($array instanceof Traversable)) { + $qp->append($array); + } + // If we are dealing with an associative array, traverse it + // and merge as we go. + elseif ($this->isAssoc($array)) { + // Do key/value substitutions + foreach ($array as $k => $v) { + + // If no dot or hash, assume class. + $first = substr($k,0,1); + if ($first != '.' && $first != '#') $k = '.' . $k; + + // If value is an array, recurse. + if (is_array($v)) { + // XXX: Not totally sure that starting at the + // top is right. Perhaps it should start + // at some other context? + $this->tplArrayR($qp->top($k), $v, $options); + } + // Otherwise, try to append value. + else { + $qp->branch()->children($k)->append($v); + } + } + } + // Otherwise we have an indexed array, and we iterate through + // it. + else { + // Get a copy of the current template and then recurse. + foreach ($array as $entry) { + $eles = $qp->get(); + $template = array(); + + // We manually deep clone the template. + foreach ($eles as $ele) { + $template = $ele->cloneNode(TRUE); + } + $tpl = qp($template); + $tpl = $this->tplArrayR($tpl, $entry, $options); + $qp->before($tpl); + } + // Remove the original template without loosing a handle to the + // newly injected one. + $dead = $qp->branch(); + $qp->parent(); + $dead->remove(); + unset($dead); + } + return $qp; + } + + /** + * Check whether an array is associative. + * If the keys of the array are not consecutive integers starting with 0, + * this will return false. + * + * @param array $array + * The array to test. + * @return Boolean + * TRUE if this is an associative array, FALSE otherwise. + */ + public function isAssoc($array) { + $i = 0; + foreach ($array as $k => $v) if ($k !== $i++) return TRUE; + // If we get here, all keys passed. + return FALSE; + } + + /** + * Convert a function name to a CSS class selector (e.g. myFunc becomes '.myFunc'). + * @param string $mname + * Method name. + * @return string + * CSS 3 Class Selector. + */ + protected function method2class($mname) { + return '.' . substr($mname, 3); + } +} +QueryPathExtensionRegistry::extend('QPTPL');
\ No newline at end of file diff --git a/lib/querypath/Extension/QPXML.php b/lib/querypath/Extension/QPXML.php new file mode 100644 index 0000000..e91fa6e --- /dev/null +++ b/lib/querypath/Extension/QPXML.php @@ -0,0 +1,209 @@ +<?php +/** @file + * XML extensions. See QPXML. + */ +/** + * Provide QueryPath with additional XML tools. + * + * @author M Butcher <matt@aleph-null.tv> + * @author Xander Guzman <theshadow@shadowpedia.info> + * @license http://opensource.org/licenses/lgpl-2.1.php LGPL or MIT-like license. + * @see QueryPathExtension + * @see QueryPathExtensionRegistry::extend() + * @see QPXML + * @ingroup querypath_extensions + */ +class QPXML implements QueryPathExtension { + + protected $qp; + + public function __construct(QueryPath $qp) { + $this->qp = $qp; + } + + public function schema($file) { + $doc = $this->qp->branch()->top()->get(0)->ownerDocument; + + if (!$doc->schemaValidate($file)) { + throw new QueryPathException('Document did not validate against the schema.'); + } + } + + /** + * Get or set a CDATA section. + * + * If this is given text, it will create a CDATA section in each matched element, + * setting that item's value to $text. + * + * If no parameter is passed in, this will return the first CDATA section that it + * finds in the matched elements. + * + * @param string $text + * The text data to insert into the current matches. If this is NULL, then the first + * CDATA will be returned. + * + * @return mixed + * If $text is not NULL, this will return a {@link QueryPath}. Otherwise, it will + * return a string. If no CDATA is found, this will return NULL. + * @see comment() + * @see QueryPath::text() + * @see QueryPath::html() + */ + public function cdata($text = NULL) { + if (isset($text)) { + // Add this text as CDATA in the current elements. + foreach ($this->qp->get() as $element) { + $cdata = $element->ownerDocument->createCDATASection($text); + $element->appendChild($cdata); + } + return $this->qp;; + } + + // Look for CDATA sections. + foreach ($this->qp->get() as $ele) { + foreach ($ele->childNodes as $node) { + if ($node->nodeType == XML_CDATA_SECTION_NODE) { + // Return first match. + return $node->textContent; + } + } + } + return NULL; + // Nothing found + } + + /** + * Get or set a comment. + * + * This function is used to get or set comments in an XML or HTML document. + * If a $text value is passed in (and is not NULL), then this will add a comment + * (with the value $text) to every match in the set. + * + * If no text is passed in, this will return the first comment in the set of matches. + * If no comments are found, NULL will be returned. + * + * @param string $text + * The text of the comment. If set, a new comment will be created in every item + * wrapped by the current {@link QueryPath}. + * @return mixed + * If $text is set, this will return a {@link QueryPath}. If no text is set, this + * will search for a comment and attempt to return the string value of the first + * comment it finds. If no comment is found, NULL will be returned. + * @see cdata() + */ + public function comment($text = NULL) { + if (isset($text)) { + foreach ($this->qp->get() as $element) { + $comment = $element->ownerDocument->createComment($text); + $element->appendChild($comment); + } + return $this->qp; + } + foreach ($this->qp->get() as $ele) { + foreach ($ele->childNodes as $node) { + if ($node->nodeType == XML_COMMENT_NODE) { + // Return first match. + return $node->textContent; + } + } + } + } + + /** + * Get or set a processor instruction. + */ + public function pi($prefix = NULL, $text = NULL) { + if (isset($text)) { + foreach ($this->qp->get() as $element) { + $comment = $element->ownerDocument->createProcessingInstruction($prefix, $text); + $element->appendChild($comment); + } + return $this->qp; + } + foreach ($this->qp->get() as $ele) { + foreach ($ele->childNodes as $node) { + if ($node->nodeType == XML_PI_NODE) { + + if (isset($prefix)) { + if ($node->tagName == $prefix) { + return $node->textContent; + } + } + else { + // Return first match. + return $node->textContent; + } + } + } // foreach + } // foreach + } + public function toXml() { + return $this->qp->document()->saveXml(); + } + + /** + * Create a NIL element. + * + * @param string $text + * @param string $value + * @reval object $element + */ + public function createNilElement($text, $value) { + $value = ($value)? 'true':'false'; + $element = $this->qp->createElement($text); + $element->attr('xsi:nil', $value); + return $element; + } + + /** + * Create an element with the given namespace. + * + * @param string $text + * @param string $nsUri + * The namespace URI for the given element. + * @retval object + */ + public function createElement($text, $nsUri = null) { + if (isset ($text)) { + foreach ($this->qp->get() as $element) { + if ($nsUri === null && strpos($text, ':') !== false) { + $ns = array_shift(explode(':', $text)); + $nsUri = $element->ownerDocument->lookupNamespaceURI($ns); + + if ($nsUri === null) { + throw new QueryPathException("Undefined namespace for: " . $text); + } + } + + $node = null; + if ($nsUri !== null) { + $node = $element->ownerDocument->createElementNS( + $nsUri, + $text + ); + } else { + $node = $element->ownerDocument->createElement($text); + } + return qp($node); + } + } + return; + } + + /** + * Append an element. + * + * @param string $text + * @retval object QueryPath + */ + public function appendElement($text) { + if (isset ($text)) { + foreach ($this->qp->get() as $element) { + $node = $this->qp->createElement($text); + qp($element)->append($node); + } + } + return $this->qp; + } +} +QueryPathExtensionRegistry::extend('QPXML'); diff --git a/lib/querypath/Extension/QPXSL.php b/lib/querypath/Extension/QPXSL.php new file mode 100644 index 0000000..4adb317 --- /dev/null +++ b/lib/querypath/Extension/QPXSL.php @@ -0,0 +1,75 @@ +<?php +/** @file + * Provide QueryPath with XSLT support using the PHP libxslt module. + * + * This is called 'QPXSL' instead of 'QPXSLT' in accordance with the name + * of the PHP extension that provides libxslt support. + * + * You must have PHP XSL support for this to function. + * + * @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 QPXSL + * @see QPXML + */ + +/** + * Provide tools for running XSL Transformation (XSLT) on a document. + * + * This extension provides the {@link QPXSL::xslt()} function, which transforms + * a source XML document into another XML document according to the rules in + * an XSLT document. + * + * This QueryPath extension can be used as follows: + * <code> + * <?php + * require 'QueryPath/QueryPath.php'; + * require 'QueryPath/Extension/QPXSL.php'; + * + * qp('src.xml')->xslt('stylesheet.xml')->writeXML(); + * ?> + * + * This will transform src.xml according to the XSLT rules in + * stylesheet.xml. The results are returned as a QueryPath object, which + * is written to XML using {@link QueryPath::writeXML()}. + * </code> + * + * @ingroup querypath_extensions + */ +class QPXSL implements QueryPathExtension { + + protected $src = NULL; + + public function __construct(QueryPath $qp) { + $this->src = $qp; + } + + /** + * Given an XSLT stylesheet, run a transformation. + * + * This will attempt to read the provided stylesheet and then + * execute it on the current source document. + * + * @param mixed $style + * This takes a QueryPath object or <em>any</em> of the types that the + * {@link qp()} function can take. + * @return QueryPath + * A QueryPath object wrapping the transformed document. Note that this is a + * <i>different</em> document than the original. As such, it has no history. + * You cannot call {@link QueryPath::end()} to undo a transformation. (However, + * the original source document will remain unchanged.) + */ + public function xslt($style) { + if (!($style instanceof QueryPath)) { + $style = qp($style); + } + $sourceDoc = $this->src->top()->get(0)->ownerDocument; + $styleDoc = $style->get(0)->ownerDocument; + $processor = new XSLTProcessor(); + $processor->importStylesheet($styleDoc); + return qp($processor->transformToDoc($sourceDoc)); + } +} +QueryPathExtensionRegistry::extend('QPXSL');
\ No newline at end of file |