Skip to content

Instantly share code, notes, and snippets.

@EGreg
Created April 23, 2013 22:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save EGreg/5448157 to your computer and use it in GitHub Desktop.
Save EGreg/5448157 to your computer and use it in GitHub Desktop.
testing
<?php
/**
* @module Db
*/
class Db_Mysql implements iDb
{
/**
* This class lets you create and use PDO database connections.
* @class Db_Mysql
* @extends iDb
* @constructor
*
* @param {string} $conn_name The name of the connection out of the connections added with Db::setConnection()
* This is required for actually connecting to the database.
* @param {PDO} [$pdo=null] Existing PDO connection. Only accepts connections to MySQL.
*/
function __construct ($conn_name, PDO $pdo = null)
{
$this->conn_name = $conn_name;
if ($pdo) {
// The following statement may throw an exception, which is fine.
$driver_name = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
if (strtolower($driver_name) != 'mysql')
throw new Exception("the PDO object is not for mysql", -1);
$this->pdo = $pdo;
}
}
/**
* The PDO connection that this object uses
* @property $pdo
* @type PDO
*/
public $pdo;
/**
* The array of all pdo objects that have been constructed
* @property $pdo_array
* @type array
* @protected
* @default array()
*/
protected static $pdo_array = array();
/**
* The name of the connection
* @property $conn_name
* @type string
* @protected
*/
protected $conn_name;
/**
* The name of the shard currently selected with reallyConnect, if any
* @property $shard_name
* @type string
* @protected
*/
protected $shard_name;
/**
* The database name of the shard currently selected with reallyConnect, if any
* @property $dbname
* @type string
*/
public $dbname;
/**
* The prefix of the shard currently selected with reallyConnect, if any
* @property $prefix
* @type string
*/
public $prefix;
/**
* Actually makes a connection to the database (by creating a PDO instance)
* @method reallyConnect
* @param {array} [$shard_name=null] A shard name that was added using Db::setShard.
* This modifies how we connect to the database.
* @return {PDO} The PDO object for connection
*/
function reallyConnect($shard_name = null)
{
$conn_name = $this->conn_name;
$conn_info = Db::getConnection($conn_name);
if (empty($conn_info)) {
throw new Exception("database connection \"$conn_name\" wasn't registered with Db.", -1);
}
if (empty($shard_name)) {
$shard_name = '';
}
$modifications = Db::getShard($conn_name, $shard_name);
if (!isset($modifications)) {
$modifications = array();
}
if (class_exists('Q')) {
/**
* @event Db/reallyConnect {before}
* @param {Db_Mysql} 'db'
* @param {string} 'shard_name'
* @param {array} 'modifications'
* @return {array}
* Extra modifications
*/
$more = Q::event('Db/reallyConnect', array(
'db' => $this,
'shard_name' => $shard_name,
'modifications' => $modifications
), 'before');
if ($more) {
$modifications = array_merge($modifications, $more);
}
}
$dsn = isset($modifications['dsn']) ? $modifications['dsn'] : $conn_info['dsn'];
$prefix = isset($modifications['prefix']) ? $modifications['prefix'] : $conn_info['prefix'];
$username = isset($modifications['username']) ? $modifications['username'] : $conn_info['username'];
$password = isset($modifications['password']) ? $modifications['password'] : $conn_info['password'];
$driver_options = isset($modifications['driver_options'])
? $modifications['driver_options']
: isset($conn_info['driver_options']) ? $conn_info['driver_options'] : null;
// More dsn changes
$dsn_fields = array();
foreach (array('host', 'port', 'dbname', 'unix_socket', 'charset') as $f) {
if (isset($modifications[$f])) {
$dsn_fields[$f] = $modifications[$f];
}
}
if ($dsn_fields) {
$dsn_array = array_merge(Db::parseDsnString($dsn), $dsn_fields);
$dsn = 'mysql:'.http_build_query($dsn_array, '', ';');
} else $dsn_array = Db::parseDsnString($dsn);
// The connection may have already been made with these parameters,
// in which case we will just retrieve the existing connection.
$this->pdo = self::pdo($dsn, $username, $password, $driver_options);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->shard_name = $shard_name;
$this->dbname = $dsn_array['dbname'];
$this->prefix = $prefix;
if (class_exists('Q')) {
/**
* @event Db/reallyConnect {after}
* @param {Db_Mysql} 'db'
* @param {string} 'shard_name'
* @param {array} 'modifications'
*/
Q::event('Db/reallyConnect', array(
'db' => $this,
'shard_name' => $shard_name,
'modifications' => $modifications
), 'after');
}
return $this->pdo;
}
/**
* Returns the name of the shard currently selected with reallyConnect, if any
* @method shardName
* @return {string}
*/
function shardName()
{
return $this->shard_name;
}
/**
* Gets the key into the associative $pdo_array
* corresponding to some database credentials.
* @method pdo
* @protected
* @static
* @param {string} $dsn The dsn to create PDO
* @param {string} $username Username for connection
* @param {string} $password Passwork for connection
* @param {array} $driver_options Driver options
* @return {PDO}
*/
protected static function pdo ($dsn, $username, $password, $driver_options)
{
$key = $dsn . $username . $password . serialize($driver_options);
if (isset(self::$pdo_array[$key])) {
return self::$pdo_array[$key];
}
// Make a new connection to a database!
try {
self::$pdo_array[$key] = @new PDO($dsn, $username, $password, $driver_options);
} catch (Exception $e) {
if (class_exists('Q_Config') and Q_Config::get('Db', 'exceptions', 'log', true)) {
Q::log($e);
}
throw $e;
}
return self::$pdo_array[$key];
}
/**
* Forwards all other calls to the PDO object
* @method __call
* @param {string} $name The function name
* @param {array} $arguments The arguments
* @return {mixed} The result of method call
*/
function __call ($name, array $arguments)
{
$this->reallyConnect();
if (!is_callable(array($this->pdo, $name))) {
throw new Exception("neither Db_Mysql nor PDO supports the $name function");
}
return call_user_func_array(array($this->pdo, $name), $arguments);
}
/**
* Returns the name of the connection with which this Db object was created.
* @method connectionName
* @return {string}
*/
function connectionName ()
{
return isset($this->conn_name) ? $this->conn_name : null;
}
/**
* Returns the connection info with which this Db object was created.
* @method connection
* @return {string}
*/
function connection()
{
if (isset($this->conn_name)) {
return Db::getConnection($this->conn_name);
}
return null;
}
/**
* Returns an associative array representing the dsn
* @method dsn
* @return {array}
*/
function dsn()
{
$conn_info = Db::getConnection($this->conn_name);
if (empty($conn_info['dsn'])) {
throw new Exception(
'No dsn string found for the connection '
. $this->conn_name
);
}
return Db::parseDsnString($conn_info['dsn']);
}
/**
* Returns the name of the database used
* @method dbName
* @return {string}
*/
function dbName()
{
$dsn = $this->dsn();
if (empty($dsn))
return null;
return $dsn['dbname'];
}
/**
* Creates a query to select fields from a table. Needs to be used with Db_Query::from().
* @method select
* @param {string|array} $fields The fields as strings, or array of alias=>field
* @param {string|array} $tablesThe tables as strings, or array of alias=>table
* @return {Db_Query_Mysql} The resulting Db_Query object
*/
function select ($fields, $tables = '')
{
if (empty($fields))
throw new Exception("fields not specified in call to 'select'.");
if (!isset($tables))
throw new Exception("tables not specified in call to 'select'.");
$query = new Db_Query_Mysql($this, Db_Query::TYPE_SELECT);
return $query->select($fields, $tables);
}
/**
* Creates a query to insert a record into a table
* @method insert
* @param {string} $table_into The name of the table to insert into
* @param {array} $fields=array() The fields as an array of column=>value pairs
* @return {Db_Query_Mysql} The resulting Db_Query_Mysql object
*/
function insert ($table_into, array $fields = array())
{
if (empty($table_into))
throw new Exception("table not specified in call to 'insert'.");
// $fields might be an empty array,
// but the insert will still be attempted.
$columns_list = array();
$values_list = array();
foreach ($fields as $column => $value) {
$columns_list[] = "$column";
if ($value instanceof Db_Expression) {
$values_list[] = "$value";
} else {
$values_list[] = ":$column";
}
}
$columns_string = implode(', ', $columns_list);
$values_string = implode(', ', $values_list);
$clauses = array(
'INTO' => "$table_into ($columns_string)", 'VALUES' => $values_string
);
return new Db_Query_Mysql($this, Db_Query::TYPE_INSERT, $clauses, $fields, $table_into);
}
/**
* Inserts multiple records into a single table, preparing the statement only once,
* and executes all the queries.
* @method insertManyAndExecute
* @param {string} $table_into The name of the table to insert into
* @param {array} [$records=array()] The array of records to insert.
* (The field names for the prepared statement are taken from the first record.)
* You cannot use Db_Expression objects here, because the function binds all parameters with PDO.
* @param {array} [$options=array()]
* An associative array of options, including:
*
* * "chunkSize" => The number of rows to insert at a time. Defaults to 20.
* You can also put 0 here, which means unlimited chunks, but it's not recommended.
* * "onDuplicateKeyUpdate" => You can put an array of fieldname => value pairs here,
* which will add an ON DUPLICATE KEY UPDATE clause to the query.
*/
function insertManyAndExecute ($table_into, array $records = array(), $options = array())
{
if (empty($table_into))
throw new Exception("table not specified in call to 'insertManyAndExecute'.");
if (empty($records))
return false;
$chunkSize = 20;
$onDuplicateKeyUpdate = null;
extract($options);
if ($chunkSize < 0)
return false;
// Get the columns list
foreach ($records[0] as $column => $value) {
$columns_list[] = "$column";
}
$columns_string = implode(', ', $columns_list);
$into = "$table_into ($columns_string)";
$index = 1;
$first_chunk = true;
$to_bind = array();
$record_count = count($records);
$update_fields = array();
$odku_clause = '';
if ($onDuplicateKeyUpdate) {
$odku_clause = "\n\t ON DUPLICATE KEY UPDATE ";
$parts = array();
foreach ($onDuplicateKeyUpdate as $k => $v) {
if ($v instanceof Db_Expression) {
$parts[] .= "`$k` = $v";
} else {
$parts[] .= "`$k` = :__update_$k";
$update_fields["__update_$k"] = $v;
}
}
$odku_clause .= implode(",\n\t", $parts);
}
// Execute all the queries using this prepared statement
$row_of_chunk = 1;
foreach ($records as $record) {
if ($first_chunk) {
// Prepare statement from first query
$values_list = array();
foreach ($record as $column => $value) {
if ($value instanceof Db_Expression) {
$values_list[] = "$value";
} else {
$values_list[] = ":$column" . $row_of_chunk;
}
}
$values_string = implode(', ', $values_list);
if ($index == 1) {
$q = "INSERT INTO $into VALUES ($values_string) ";
} else {
$q .= ",\n\t ($values_string) ";
}
}
foreach ($record as $column => $value) {
$to_bind[$column . $row_of_chunk] = $value;
}
++$row_of_chunk;
if ($chunkSize === 1
or ($chunkSize > 1 and $row_of_chunk % $chunkSize === 1)
or $index === $record_count) {
if ($onDuplicateKeyUpdate) {
$q .= $odku_clause;
}
$q .= ';';
$first_chunk = false;
$this->rawQuery($q)->bind($to_bind)->bind($update_fields)
->execute(true);
$row_of_chunk = 1;
}
++$index;
}
}
/**
* Creates a query to update records. Needs to be used with {@link Db_Query::set}
* @method update
* @param {string} $table The table to update
* @return {Db_Query_Mysql} The resulting Db_Query object
*/
function update ($table)
{
if (empty($table))
throw new Exception("table not specified in call to 'update'.");
$clauses = array('UPDATE' => "$table");
return new Db_Query_Mysql($this, Db_Query::TYPE_UPDATE, $clauses, array(), $table);
}
/**
* Creates a query to delete records.
* @method delete
* @param {string} $table_from The table to delete from
* @param {string} [$table_using=null] If set, adds a USING clause with this table. You can then use ->join() with the resulting Db_Query.
* @return {Db_Query_Mysql}
*/
function delete ($table_from, $table_using = null)
{
if (empty($table_from))
throw new Exception("table not specified in call to 'delete'.");
if (isset($table_using) and !is_string($table_using)) {
throw new Exception("table_using field must be a string");
}
if (isset($table_using))
$clauses = array('FROM' => "$table_from USING $table_using");
else
$clauses = array('FROM' => "$table_from");
return new Db_Query_Mysql($this, Db_Query::TYPE_DELETE, $clauses, array(), $table_from);
}
/**
* Creates a query from raw SQL
* @method rawQuery
* @param {string} $sql May contain more than one SQL statement
* @param {array} [$bind=array()] An array of parameters to bind to the query, using
* the Db_Query_Mysql->bind method.
* @return {Db_Query_Mysql}
*/
function rawQuery ($sql, $bind = array())
{
$clauses = array('RAW' => $sql);
$query = new Db_Query_Mysql($this, Db_Query::TYPE_RAW, $clauses);
if ($bind) {
$query->bind($bind);
}
return $query;
}
/**
* Creates a query to rollback a previously started transaction.
* @method update
* @param {array} $criteria The criteria to use, for sharding
* @return {Db_Query_Mysql} The resulting Db_Query object
*/
function rollback ($criteria = null)
{
$query = new Db_Query_Mysql($this, Db_Query::TYPE_ROLLBACK, array('ROLLBACK' => true));
$query->rollback($criteria);
return $query;
}
/**
* Sorts a table in chunks
* @method rank
* @param {string} $table The name of the table in the database
* @param {string} $pts_field The name of the field to rank by.
* @param {string} $rank_field The rank field to update in all the rows
* @param {integer} [$chunk_size=1000] The number of rows to process at a time. Default is 1000.
* This is so the queries don't tie up the database server for very long,
* letting it service website requests and other things.
* @param {integer} [$rank_level2=0] Since the ranking is done in chunks, the function must know
* which rows have not been processed yet. If this field is empty (default)
* then the function sets the rank_field to 0 in all the rows, before
* starting the ranking process.
* (That might be a time consuming operation.)
* Otherwise, if $rank is a nonzero integer, then the function alternates
* between the ranges
* 0 to $rank_level2, and $rank_level2 to $rank_level2 * 2.
* That is, after it is finished, all the ratings will be in one of these
* two ranges.
* If not empty, this should be a very large number, like a billion.
* @param {string} [$order_by_clause=null] The order clause to use when calculating ranks.
* Default is "ORDER BY `$pts_field` DESC"
*/
function rank(
$table,
$pts_field,
$rank_field,
$chunk_size = 1000,
$rank_level2 = 0,
$order_by_clause = null)
{
if (!isset($order_by_clause))
$order_by_clause = "ORDER BY $pts_field DESC";
if (empty($rank_level2)) {
$this->update($table)
->set(array($rank_field => 0))
->execute();
$rank_base = 0;
$condition = "$rank_field = 0 OR $rank_field IS NULL";
} else {
$rows = $this->select($pts_field, $table)
->where("$rank_field < $rank_level2")
->limit(1)
->fetchAll();
if (!empty($rows)) {
// There are no ranks above $rank_level2. Create ranks on level 2.
$rank_base = $rank_level2;
$condition = "$rank_field < $rank_level2";
} else {
// The ranks are all above $rank_level2. Create ranks on level 1.
$rank_base = 0;
$condition = "$rank_field >= $rank_level2";
}
}
// Count all the rows
$rows = $this->rawQuery("SELECT COUNT(1) _count FROM $table")->fetchAll(PDO::FETCH_ASSOC);
$count = $rows[0]['_count'];
// Here comes the magic:
$offset = 0;
$this->rawQuery("set @rank = $offset")->execute();
do {
$this->rawQuery("
UPDATE $table
SET $rank_field = $rank_base + (@rank := @rank + 1)
WHERE $condition
$order_by_clause
LIMIT $chunk_size
")->execute();
$offset += $chunk_size;
} while ($count-$offset > 0);
}
/**
* Generate an ID that is unique in a table
* @method uniqueId
* @param {string} $table The name of the table
* @param {string} $field The name of the field to check for uniqueness.
* You should probably have an index starting with this field.
* @param {array} [$where=array()] You can indicate conditions here to limit the search for
* an existing value. The result is an id that is unique within a certain partition.
* @param {array} [$options=array()]
* Optional array used to override default options:
*
* * "length" => Defaults to 7. The length of the ID to generate, after the prefix.
* * "characters" => A string of characters from which to construct the ID.
* * "prefix" => The prefix to prepend to the unique id. Defaults to ''.
* * "filter" => The name of a function that will take the generated string and
* check it. The filter function can modify the string by returning another string,
* or simply reject the string by returning false, in which another string will be generated.
*/
function uniqueId(
$table,
$field,
$where = array(),
$options = array())
{
$length = 7;
$characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890';
$prefix = '';
extract($options);
$count = strlen($characters);
do {
$id = $prefix;
for ($i=0; $i<$length; ++$i) {
$id .= $characters[mt_rand(0, $count-1)];
}
if (!empty($options['filter'])) {
$ret = Q::call($options['filter'], array(compact('id', 'table', 'field', 'where', 'options')));
if ($ret === false) {
continue;
} else if ($ret) {
$id = $ret;
}
}
$q = $this->select($field, $table)
->where(array($field => $id));
if ($where) {
$q->andWhere($where);
}
$rows = $q->limit(1)->fetchAll();
} while ($rows);
return $id;
}
/**
* Returns a timestamp from a Date string
* @method fromDate
* @param {string} $datetime The Date string that comes from the db
* @return {integer} The timestamp
*/
function fromDate ($date)
{
$year = substr($date, 0, 4);
$month = substr($date, 5, 2);
$day = substr($date, 8, 2);
return mktime(0, 0, 0, $month, $day, $year);
}
/**
* Returns a timestamp from a DateTime string
* @method fromDateTime
* @param {string} $syntax The format of the date string, see `date()` function.
* @param {string} $datetime The DateTime string that comes from the db
* @return {integer} The timestamp
*/
function fromDateTime ($datetime)
{
$year = substr($datetime, 0, 4);
$month = substr($datetime, 5, 2);
$day = substr($datetime, 8, 2);
$hour = substr($datetime, 11, 2);
$min = substr($datetime, 14, 2);
$sec = substr($datetime, 17, 2);
return mktime($hour, $min, $sec, $month, $day, $year);
}
/**
* Returns a Date string to store in the database
* @method toDate
* @param {string} $timestamp The UNIX timestamp, e.g. from a strtotime function
* @return {string}
*/
function toDate ($timestamp)
{
return date('Y-m-d', $timestamp);
}
/**
* Returns a DateTime string to store in the database
* @method toDateTime
* @param {string} $timestamp The UNIX timestamp, e.g. from a strtotime function
* @return {string}
*/
function toDateTime ($timestamp)
{
return date('Y-m-d H:i:s', $timestamp);
}
/**
* Takes a MySQL script and returns an array of queries.
* When DELIMITER is changed, respects that too.
* @method scriptToQueries
* @param {string} $script The text of the script
* @param {callable} [$callback=null] Optional callback to call for each query.
* @return {array} An array of the SQL queries.
*/
function scriptToQueries($script, $callback = null)
{
$this->reallyConnect();
$version_string = $this->pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
$version_parts = explode('.', $version_string);
sprintf("%1d%02d%02d", $version_parts[0], $version_parts[1], $version_parts[2]);
$script_stripped = $script;
return $this->scriptToQueries_internal($script_stripped, $callback);
}
/**
* Takes stripped MySQL script and returns an array of queries.
* When DELIMITER is changed, respects that too.
* @method scriptToQueries_internal
* @protected
* @param {string} $script The text of the script
* @param {callable} [$callback=null] Optional callback to call for each query.
* @return {array} An array of the SQL queries.
*/
protected function scriptToQueries_internal($script, $callback = null)
{
$queries = array();
$script_len = strlen($script);
$this->reallyConnect();
$version_string = $this->pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
$version_parts = explode('.', $version_string);
$version = sprintf("%1d%02d%02d", $version_parts[0], $version_parts[1], $version_parts[2]);
//$mode_n = 0; // normal
$mode_c = 1; // comments
$mode_sq = 2; // single quotes
$mode_dq = 3; // double quotes
$mode_bt = 4; // backticks
$mode_lc = 5; // line comment (hash or double-dash)
$mode_ds = 6; // delimiter statement
$cur_pos = 0;
$d = ';'; // delimiter
$d_len = strlen($d);
$query_start_pos = 0;
$del_start_pos_array = array();
$del_end_pos_array = array();
if (class_exists('Q_Config')) {
$separator = Q_Config::expect('Db', 'sql', 'querySeparator');
} else {
$separator = "\n-------- NEXT QUERY STARTS HERE --------\n";
}
$found = strpos($script, $separator);
if ($found !== false) {
// This script was specially crafted for quick parsing
$queries = explode($separator, $script);
foreach ($queries as $i => $query) {
if (!trim($query)) {
unset($queries[$i]);
}
}
return $queries;
}
while (1) {
$c_pos = strpos($script, "/*", $cur_pos);
$sq_pos = strpos($script, "'", $cur_pos);
$dq_pos = strpos($script, "\"", $cur_pos);
$bt_pos = strpos($script, "`", $cur_pos);
$c2_pos = strpos($script, "--", $cur_pos);
$c3_pos = strpos($script, "#", $cur_pos);
$ds_pos = stripos($script, "\nDELIMITER ", $cur_pos);
$next_pos = false;
if ($c_pos !== false) {
$next_mode = $mode_c;
$next_pos = $c_pos;
$next_end_str = "*/";
$next_end_str_len = 2;
}
if ($sq_pos !== false and ($next_pos === false or $sq_pos < $next_pos)) {
$next_mode = $mode_sq;
$next_pos = $sq_pos;
$next_end_str = "'";
$next_end_str_len = 1;
}
if ($dq_pos !== false and ($next_pos === false or $dq_pos < $next_pos)) {
$next_mode = $mode_dq;
$next_pos = $dq_pos;
$next_end_str = "\"";
$next_end_str_len = 1;
}
if ($bt_pos !== false and ($next_pos === false or $bt_pos < $next_pos)) {
$next_mode = $mode_bt;
$next_pos = $bt_pos;
$next_end_str = "`";
$next_end_str_len = 1;
}
if ($c2_pos !== false and ($next_pos === false or $c2_pos < $next_pos)
and ($script[$c2_pos+2] == " " or $script[$c2_pos+2] == "\t")) {
$next_mode = $mode_lc;
$next_pos = $c2_pos;
$next_end_str = "\n";
$next_end_str_len = 1;
}
if ($c3_pos !== false and ($next_pos === false or $c3_pos < $next_pos)) {
$next_mode = $mode_lc;
$next_pos = $c3_pos;
$next_end_str = "\n";
$next_end_str_len = 1;
}
if ($ds_pos !== false and ($next_pos === false or $ds_pos < $next_pos)) {
$next_mode = $mode_ds;
$next_pos = $ds_pos;
$next_end_str = "\n";
$next_end_str_len = 1;
}
// If at this point, $next_pos === false, then
// we are in the final stretch.
// Until the end of the string, we have normal mode.
// Right now, we are in normal mode.
$d_pos = strpos($script, $d, $cur_pos);
while ($d_pos !== false and ($next_pos === false or $d_pos < $next_pos)) {
$query = substr($script, $query_start_pos, $d_pos - $query_start_pos);
// remove parts of the query string based on the "del_" arrays
$del_pos_count = count($del_start_pos_array);
if ($del_pos_count == 0) {
$query2 = $query;
} else {
$query2 = substr($query, 0, $del_start_pos_array[0] - $query_start_pos);
for ($i=1; $i < $del_pos_count; ++$i) {
$query2 .= substr($query, $del_end_pos_array[$i-1] - $query_start_pos,
$del_start_pos_array[$i] - $del_end_pos_array[$i-1]);
}
$query2 .= substr($query,
$del_end_pos_array[$del_pos_count - 1] - $query_start_pos);
}
$del_start_pos_array = array(); // reset these arrays
$del_end_pos_array = array(); // reset these arrays
$query_start_pos = $d_pos + $d_len;
$cur_pos = $query_start_pos;
$query2 = trim($query2);
if ($query2)
$queries[] = $query2; // <----- here is where we add to the main array
if ($callback) {
call_user_func($callback, $query2);
}
$d_pos = strpos($script, $d, $cur_pos);
};
if ($next_pos === false) {
// Add the last query and get out of here:
$query = substr($script, $query_start_pos);
// remove parts of the query string based on the "del_" arrays
$del_pos_count = count($del_start_pos_array);
if ($del_pos_count == 0) {
$query2 = $query;
} else {
$query2 = substr($query, 0, $del_start_pos_array[0] - $query_start_pos);
for ($i=1; $i < $del_pos_count; ++$i) {
$query2 .= substr($query, $del_end_pos_array[$i-1] - $query_start_pos,
$del_start_pos_array[$i] - $del_end_pos_array[$i-1]);
}
if ($del_end_pos_array[$del_pos_count - 1] !== false) {
$query2 .= substr($query,
$del_end_pos_array[$del_pos_count - 1] - $query_start_pos);
}
}
$query2 = trim($query2);
if ($query2) {
$queries[] = $query2;
if ($callback) {
call_user_func($callback, $query2);
}
}
break;
}
if ($next_mode == $mode_c) {
// We are inside a comment
$end_pos = strpos($script, $next_end_str, $next_pos + 1);
if ($end_pos === false) {
throw new Exception("unterminated comment -- missing terminating */ characters.");
}
$version_comment = false;
if ($script[$next_pos + 2] == '!') {
$ver = substr($script, $next_pos + 3, 5);
if ($version >= $ver) {
// we are in a version comment
$version_comment = true;
}
}
// Add to list of areas to ignore
if ($version_comment) {
$del_start_pos_array[] = $next_pos;
$del_end_pos_array[] = $next_pos + 3 + 5;
$del_start_pos_array[] = $end_pos;
$del_end_pos_array[] = $end_pos + $next_end_str_len;
} else {
$del_start_pos_array[] = $next_pos;
$del_end_pos_array[] = $end_pos + $next_end_str_len;
}
} else if ($next_mode == $mode_lc) {
// We are inside a line comment
$end_pos = strpos($script, $next_end_str, $next_pos + 1);
$del_start_pos_array[] = $next_pos;
if ($end_pos !== false) {
$del_end_pos_array[] = $end_pos + $next_end_str_len;
} else {
$del_end_pos_array[] = false;
}
} else if ($next_mode == $mode_ds) {
// We are inside a DELIMITER statement
$start_pos = $next_pos;
$end_pos = strpos($script, $next_end_str, $next_pos + 11);
$del_start_pos_array[] = $next_pos;
if ($end_pos !== false) {
$del_end_pos_array[] = $end_pos + $next_end_str_len;
} else {
// this is the last statement in the script, it seems.
// Might look funny, like:
// DELIMITER aa sfghjkhsgkjlfhdsgjkfdglhdfsgkjfhgjdlk
$del_end_pos_array[] = false;
}
// set a new delimiter!
$try_d = trim(substr($script, $ds_pos + 11, $end_pos - ($ds_pos + 11)));
if (!empty($try_d)) {
$d = $try_d;
$d_len = strlen($d);
} // otherwise malformed delimiter statement or end of file
} else {
// We are inside a string
$start_pos = $next_pos;
$try_end_pos = $next_pos;
do {
$end_pos = false;
$try_end_pos = strpos($script, $next_end_str, $try_end_pos + 1);
if ($try_end_pos === false) {
throw new Exception("unterminated string -- missing terminating $next_end_str character.");
}
if ($try_end_pos+1 >= $script_len) {
$end_pos = $try_end_pos;
break;
}
if ($script[$try_end_pos+1] == $next_end_str) {
++$try_end_pos;
continue;
}
$bs_count = 0;
for ($i = $try_end_pos - 1; $i > $next_pos; --$i) {
if ($script[$i] == "\\") {
++$bs_count;
} else {
break;
}
}
if ($bs_count % 2 == 0) {
$end_pos = $try_end_pos;
}
} while ($end_pos === false);
// If we are here, we have found the end of the string,
// and are back in normal mode.
}
// We have exited the previous mode and set end_pos.
if ($end_pos === false)
break;
$cur_pos = $end_pos + $next_end_str_len;
}
foreach ($queries as $i => $query) {
if ($query === false) {
unset($queries[$i]);
}
}
return $queries;
}
/**
* Generates base classes of the models, and if they don't exist,
* skeleton code for the models themselves.
* Use it only after you have made changes to the database schema.
* You shouldn't be using it on every request.
* @method generateModels
* @param {string} $directory The directory in which to generate the files.
* If the files already exist, they are not overwritten,
* unless they are inside the "Base" subdirectory.
* If the "Base" subdirectory does not exist, it is created.
* @param {string} [$classname_prefix=null] The prefix to prepend to the Base class names.
* If not specified, prefix becomes "Conn_Name_", where conn_name is the name of the connection.
* @throws {Exception} If the $connection is not registered, or the $directory
* does not exist, this function throws an exception.
*/
function generateModels (
$directory,
$classname_prefix = null)
{
$dc = '/**';
if (!file_exists($directory))
throw new Exception("directory $directory does not exist.");
$conn_name = $this->connectionName();
$conn = Db::getConnection($conn_name);
$prefix = empty($conn['prefix']) ? '' : $conn['prefix'];
$prefix_len = strlen($prefix);
if (!isset($classname_prefix)) {
$classname_prefix = isset($conn_name) ? $conn_name . '_' : '';
}
$rows = $this->rawQuery('SHOW TABLES')->fetchAll();
if (class_exists('Q_Config')) {
$ext = Q_Config::get('Q', 'extensions', 'class', 'php');
} else {
$ext = 'php';
}
$table_classnames = array();
$js_table_classes_string = '';
$class_name_prefix = rtrim(ucfirst($classname_prefix), "._");
foreach ($rows as $row) {
uksort
$table_name = $row[0];
$table_name_base = substr($table_name, $prefix_len);
$table_name_prefix = substr($table_name, 0, $prefix_len);
if (empty($table_name_base) or $table_name_prefix != $prefix or stristr($table_name, '_Q_') !== false)
continue; // no class generated
$class_name_base = null;
$js_base_class_string = '';
$base_class_string = $this->codeForModelBaseClass(
$table_name,
$directory,
$classname_prefix,
$class_name_base,
null,
$js_base_class_string,
$table_comment
); // sets the $class_name variable
$class_name = ucfirst($classname_prefix) . $class_name_base;
if (empty($class_name))
continue; // no class generated
$class_name_parts = explode('_', $class_name);
$class_filename = $directory.DS.implode(DS, $class_name_parts).'.php';
$base_class_filename = $directory.DS.'Base'.DS.implode(DS, $class_name_parts).'.php';
$js_class_filename = $directory.DS.implode(DS, $class_name_parts).'.js';
$js_base_class_filename = $directory.DS.'Base'.DS.implode(DS, $class_name_parts).'.js';
$js_base_class_require = 'Base'.DS.implode(DS, $class_name_parts);
$js_class_name = implode('.', $class_name_parts);
$js_base_class_name = implode('.Base.', $class_name_parts);
$class_extras = is_readable($class_filename.'.inc') ? file_get_contents($class_filename.'.inc') : '';
$js_class_extras = is_readable($js_class_filename.'.inc') ? file_get_contents($js_class_filename.'.inc') : '';
$class_string = <<<EOT
<?php
$dc
* @module $conn_name
*/
$dc
* Class representing '$class_name_base' rows in the '$conn_name' database
* You can create an object of this class either to
* access its non-static methods, or to actually
* represent a $table_name_base row in the $conn_name database.
*
* @class $class_name
* @extends Base_$class_name
*/
class $class_name extends Base_$class_name
{
$dc
* The setUp() method is called the first time
* an object of this class is constructed.
* @method setUp
*/
function setUp()
{
parent::setUp();
// INSERT YOUR CODE HERE
// e.g. \$this->hasMany(...) and stuff like that.
}
/*
* Add static methods here.
* If file '$class_name_base.php.inc' exists, its content is included
* * * */
$class_extras
/* * * */
$dc
* Implements the __set_state method, so it can work with
* with var_export and be re-imported successfully.
* @method __set_state
* @param {array} \$array
* @return {{$class_name}} Class instance
*/
static function __set_state(array \$array) {
\$result = new $class_name();
foreach(\$array as \$k => \$v)
\$result->\$k = \$v;
return \$result;
}
};
EOT;
$js_class_string = <<<EOT
$dc
* Class representing $table_name_base rows.
*
* This description should be revised and expanded.
*
* @module $conn_name
*/
var Q = require('Q');
var Db = Q.require('Db');
$dc
* Class representing '$class_name_base' rows in the '$conn_name' database
$table_comment * @namespace $class_name_prefix
* @class $class_name_base
* @extends Base.$js_class_name
* @constructor
* @param fields {object} The fields values to initialize table row as
* an associative array of `{column: value}` pairs
*/
function $class_name (fields) {
$dc
* The setUp() method is called the first time
* an object of this class is constructed.
* @method setUp
*/
this.setUp = function () {
// put any code here
};
// Run constructors of mixed in objects
this.constructors.call(this, arguments);
/*
* Add any other methods to the model class by assigning them to this.
* If file '$class_name_base.js.inc' exists, its content is included
* * * */
$js_class_extras
/* * * */
}
Q.mixin($class_name, Q.require('$js_base_class_require'));
module.exports = $class_name;
EOT;
// overwrite base class file if necessary, but not the class file
Db_Utils::saveTextFile($base_class_filename, $base_class_string);
Db_Utils::saveTextFile($js_base_class_filename, $js_base_class_string);
if (! file_exists($class_filename)) {
Db_Utils::saveTextFile($class_filename, $class_string);
}
if (! file_exists($js_class_filename)) {
Db_Utils::saveTextFile($js_class_filename, $js_class_string);
}
$table_classnames[] = $class_name;
$js_table_classes_string .= <<<EOT
$dc
* Link to $conn_name.$class_name_base model
* @property $class_name_base
* @type $conn_name.$class_name_base
*/
this.$class_name_base = Q.require('$conn_name/$class_name_base');
EOT;
}
// Generate the "module model" base class file
$table_classnames_exported = var_export($table_classnames, true);
$table_classnames_json = $pk_json_indented = str_replace(
array("[", ",", "]"),
array("[\n\t\t", ",\n\t\t", "\n\t]"),
json_encode($table_classnames)
);
if (!empty($conn_name)) {
$class_name = Db::generateTableClassName($conn_name);
$class_name_parts = explode('_', $class_name);
$class_filename = $directory.DS.implode(DS, $class_name_parts).'.php';
$base_class_filename = $directory.DS.'Base'.DS.implode(DS, $class_name_parts).'.php';
$js_class_filename = $directory.DS.implode(DS, $class_name_parts).'.js';
$js_base_class_filename = $directory.DS.'Base'.DS.implode(DS, $class_name_parts).'.js';
$js_base_class_require = 'Base'.DS.implode(DS, $class_name_parts);
// because table name can be {$prefix}_Q_plugin or {$prefix}_Q_app we need to know correct table name
$tables = $this->rawQuery("SHOW TABLES LIKE '{$prefix}Q_%'")->execute()->fetchAll(PDO::FETCH_NUM);
if ($tables) {
$tablename = $tables[0][0];
$model_comment = $this->rawQuery("SELECT * FROM $tablename")->execute()->fetchAll(PDO::FETCH_NUM);
$model_comment = (isset($model_comment[0]) && !empty($model_comment[0][2])) ? " * <br/>{$model_comment[0][2]}\n" : '';
} else {
$model_comment = '';
}
$model_extras = is_readable($class_filename.'.inc') ? file_get_contents($class_filename.'.inc') : '';
$js_model_extras = is_readable($js_class_filename.'.inc') ? file_get_contents($js_class_filename.'.inc') : '';
$base_class_string = <<<EOT
<?php
$dc
* Autogenerated base class for the $conn_name model.
*
* Don't change this file, since it can be overwritten.
* Instead, change the $class_name.php file.
*
* @module $conn_name
*/
$dc
* Base class for the $class_name model
* @class Base_$class_name
*/
abstract class Base_$class_name
{
$dc
* The list of model classes
* @property \$table_classnames
* @type array
*/
static \$table_classnames = $table_classnames_exported;
$dc
* This method uses Db::connect() to establish a connection to database using information stored in the configuration.
* If the connection to Db object has already been made, it returns this Db object.
* @method db
* @return {iDb} The database object
*/
static function db()
{
return Db::connect('$conn_name');
}
$dc
* The connection name for the class
* @method connectionName
* @return {string} The name of the connection
*/
static function connectionName()
{
return '$conn_name';
}
};
EOT;
$js_base_class_string = <<<EOT
$dc
* Autogenerated base class for the $conn_name model.
*
* Don't change this file, since it can be overwritten.
* Instead, change the $class_name.js file.
*
* @module $conn_name
*/
var Q = require('Q');
var Db = Q.require('Db');
$dc
* Base class for the $class_name model
* @namespace Base
* @class $class_name
* @static
*/
module.exports = function () {
$dc
* The list of model classes
* @property tableClasses
* @type array
*/
this.tableClasses = $table_classnames_json;
$dc
* This method uses Db.connect() to establish a connection to database using information stored in the configuration.
* If the connection to Db object has already been made, it returns this Db object.
* @method db
* @return {Db} The database connection
*/
this.db = function () {
return Db.connect('$conn_name');
};
$dc
* The connection name for the class
* @method connectionName
* @return {string} The name of the connection
*/
this.connectionName = function() {
return '$conn_name';
};
$js_table_classes_string
return this;
};
EOT;
$class_string = <<<EOT
<?php
$dc
* $class_name_prefix model
$model_comment * @module $conn_name
* @main $conn_name
*/
$dc
* Static methods for the $conn_name models.
* @class $class_name
* @extends Base_$class_name
*/
abstract class $class_name extends Base_$class_name
{
/*
* This is where you would place all the static methods for the models,
* the ones that don't strongly pertain to a particular row or table.
* If file '$class_name.php.inc' exists, its content is included
* * * */
$model_extras
/* * * */
};
EOT;
$js_class_string = <<<EOT
$dc
* $class_name_prefix model
$model_comment * @module $conn_name
* @main $conn_name
*/
var Q = require('Q');
$dc
* Static methods for the $class_name_prefix model
* @class $class_name_prefix
* @extends Base.$class_name_prefix
* @static
*/
var $conn_name = module.exports;
Q.require('$js_base_class_require').apply($conn_name);
/*
* This is where you would place all the static methods for the models,
* the ones that don't strongly pertain to a particular row or table.
* Just assign them as methods of the $conn_name object.
* If file '$class_name.js.inc' exists, its content is included
* * * */
$js_model_extras
/* * * */
EOT;
// overwrite base class file if necessary, but not the class file
Db_Utils::saveTextFile($base_class_filename, $base_class_string);
Db_Utils::saveTextFile($js_base_class_filename, $js_base_class_string);
if (! file_exists($class_filename)) {
Db_Utils::saveTextFile($class_filename, $class_string);
}
if (! file_exists($js_class_filename)) {
Db_Utils::saveTextFile($js_class_filename, $js_class_string);
}
}
}
/**
* Generates code for a base class for the model
* @method codeForModelBaseClass
* @param {string} $table The name of the table to generate the code for.
* @param {string} $directory The path of the directory in which to place the model code.
* @param {string} [$classname_prefix=''] The prefix to prepend to the generated class names
* @param {&string} [$class_name_base=null] If set, this is the class name that is used.
* If an unset variable is passed, it is filled with the
* class name that is ultimately chosen, without the $classname_prefix
* @param {string} [$prefix=null] Defaults to the prefix of the tables, as specified in the connection.
* Pass null here to use the default, or a string to override it.
* @param {&string} [$js_code=null] The javascript code for the base class
* @param {&string} [$table_comment=''] The comment from the MySQL table if any
* @return {string} The generated code for the class.
*/
function codeForModelBaseClass (
$table_name,
$directory,
$classname_prefix = '',
&$class_name_base = null,
$prefix = null,
&$js_code = null,
&$table_comment = '')
{
$dc = '/**';
if (empty($table_name))
throw new Exception('table_name parameter is empty', - 2);
if (empty($directory))
throw new Exception('directory parameter is empty', - 3);
$conn_name = $this->connectionName();
$conn = Db::getConnection($conn_name);
if (!isset($prefix)) {
$prefix = empty($conn['prefix']) ? '' : $conn['prefix'];
}
if (!empty($prefix)) {
$prefix_len = strlen($prefix);
$table_name_base = substr($table_name, $prefix_len);
$table_name_prefix = substr($table_name, 0, $prefix_len);
if (empty($table_name_base) or $table_name_prefix != $prefix)
return ''; // no class generated
} else {
$table_name_base = $table_name;
}
if (empty($classname_prefix))
$classname_prefix = '';
if (!isset($class_name_base)) {
$class_name_base = Db::generateTableClassName($table_name_base);
}
$class_name = ucfirst($classname_prefix) . $class_name_base;
$table_cols = $this->rawQuery("SHOW COLUMNS FROM $table_name")->execute()->fetchAll(PDO::FETCH_ASSOC);
$table_status = $this->rawQuery("SHOW TABLE STATUS WHERE Name = '$table_name'")->execute()->fetchAll(PDO::FETCH_COLUMN, 17);
$table_comment = (!empty($table_status[0])) ? " * <br/>{$table_status[0]}\n" : '';
// Calculate primary key
$pk = array();
foreach ($table_cols as $table_col) {
if ($table_col['Key'] == 'PRI')
$pk[] = $table_col['Field'];
}
$pk_exported = var_export($pk, true);
$pk_json = json_encode($pk);
// Calculate validation functions
$functions = array();
$js_functions = array();
$field_names = array();
$properties = array();
$js_properties = array();
$required_field_names = array();
$magic_field_names = array();
foreach ($table_cols as $table_col) {
$is_magic_field = null;
$field_name = $table_col['Field'];
$field_names[] = $field_name;
$field_null = $table_col['Null'] == 'YES' ? true : false;
$field_default = $table_col['Default'];
$auto_inc = strpos($table_col['Extra'], 'auto_increment') !== false ? true : false;
$type = $table_col['Type'];
$pieces = explode('(', $type);
if (isset($pieces[1])) {
$pieces2 = explode(')', $pieces[1]);
$pieces2_count = count($pieces2);
if ($pieces2_count > 2) {
$pieces2 = array(
implode(')', array_slice($pieces2, 0, -1)),
end($pieces2)
);
}
}
$type_name = $pieces[0];
if (isset($pieces2)) {
$type_display_range = $pieces2[0];
$type_modifiers = $pieces2[1];
$type_unsigned = (strpos($type_modifiers, 'unsigned') !== false);
}
switch ($type_name) {
case 'tinyint':
$type_range_min = $type_unsigned ? 0 : - 128;
$type_range_max = $type_unsigned ? 255 : 127;
break;
case 'smallint':
$type_range_min = $type_unsigned ? 0 : - 32768;
$type_range_max = $type_unsigned ? 65535 : 32767;
break;
case 'mediumint':
$type_range_min = $type_unsigned ? 0 : - 8388608;
$type_range_max = $type_unsigned ? 16777215 : 8388607;
break;
case 'int':
$type_range_min = $type_unsigned ? 0 : - 2147483648;
$type_range_max = $type_unsigned ? 4294967295 : 2147483647;
break;
case 'bigint':
$type_range_min = $type_unsigned ? 0 : - 9223372036854775808;
$type_range_max = $type_unsigned ? 18446744073709551615 : 9223372036854775807;
break;
}
$null_check = $field_null ? "if (!isset(\$value)) return array('$field_name', \$value);\n\t\t" : '';
$dbe_check = "if (\$value instanceof Db_Expression) return array('$field_name', \$value);\n\t\t";
$js_null_check = $field_null ? "if (!value) return value;\n\t\t" : '';
$js_dbe_check = "if (value instanceof Db.Expression) return value;\n\t\t";
if (! isset($functions["beforeSet_$field_name"]))
$functions["beforeSet_$field_name"] = array();
if (! isset($js_functions["beforeSet_$field_name"]))
$js_functions["beforeSet_$field_name"] = array();
switch (strtolower($type_name)) {
case 'tinyint':
case 'smallint':
case 'int':
case 'mediumint':
case 'bigint':
$properties[]="integer $field_name";
$js_properties[] = "$field_name integer";
$functions["beforeSet_$field_name"][] = <<<EOT
{$null_check}{$dbe_check}if (!is_numeric(\$value) or floor(\$value) != \$value)
throw new Exception('Non-integer value being assigned to '.\$this->getTable().".$field_name");
if (\$value < $type_range_min or \$value > $type_range_max)
throw new Exception("Out-of-range value '\$value' being assigned to ".\$this->getTable().".$field_name");
EOT;
$functions["beforeSet_$field_name"]['comment'] = <<<EOT
$dc
* Method is called before setting the field and verifies if integer value falls within allowed limits
* @method beforeSet_$field_name
* @param {integer} \$value
* @return {array} An array of field name and value
* @throws {Exception} An exception is thrown if \$value is not integer or does not fit in allowed range
*/
EOT;
$js_functions["beforeSet_$field_name"][] = <<<EOT
{$js_null_check}{$js_dbe_check}value = Number(value);
if (isNaN(value) || Math.floor(value) != value)
throw new Error('Non-integer value being assigned to '+this.table()+".$field_name");
if (value < $type_range_min || value > $type_range_max)
throw new Error("Out-of-range value '"+value+"' being assigned to "+this.table()+".$field_name");
EOT;
$js_functions["beforeSet_$field_name"]['comment'] = <<<EOT
$dc
* Method is called before setting the field and verifies if integer value falls within allowed limits
* @method beforeSet_$field_name
* @param {integer} value
* @return {integer} The value
* @throws {Error} An exception is thrown if 'value' is not integer or does not fit in allowed range
*/
EOT;
break;
case 'enum':
$properties[]="mixed $field_name";
$js_properties[] = "$field_name string";
$functions["beforeSet_$field_name"][] = <<<EOT
{$null_check}{$dbe_check}if (!in_array(\$value, array($type_display_range)))
throw new Exception("Out-of-range value '\$value' being assigned to ".\$this->getTable().".$field_name");
EOT;
$functions["beforeSet_$field_name"]['comment'] = <<<EOT
$dc
* Method is called before setting the field and verifies if value belongs to enum values list
* @method beforeSet_$field_name
* @param {string} \$value
* @return {array} An array of field name and value
* @throws {Exception} An exception is thrown if \$value does not belong to enum values list
*/
EOT;
$js_functions["beforeSet_$field_name"][] = <<<EOT
{$js_null_check}{$js_dbe_check}if ([$type_display_range].indexOf(value) < 0)
throw new Error("Out-of-range value '"+value+"' being assigned to "+this.table()+".$field_name");
EOT;
$js_functions["beforeSet_$field_name"]['comment'] = <<<EOT
$dc
* Method is called before setting the field and verifies if value belongs to enum values list
* @method beforeSet_$field_name
* @param {string} value
* @return {string} The value
* @throws {Error} An exception is thrown if 'value' does not belong to enum values list
*/
EOT;
break;
case 'varchar':
case 'varbinary':
$properties[]="string $field_name";
$js_properties[] = "$field_name string";
$functions["beforeSet_$field_name"][] = <<<EOT
{$null_check}{$dbe_check}if (!is_string(\$value) and !is_numeric(\$value))
throw new Exception('Must pass a string to '.\$this->getTable().".$field_name");
if (strlen(\$value) > $type_display_range)
throw new Exception('Exceedingly long value being assigned to '.\$this->getTable().".$field_name");
EOT;
$functions["beforeSet_$field_name"]['comment'] = <<<EOT
$dc
* Method is called before setting the field and verifies if value is string of length within acceptable limit.
* Optionally accept numeric value which is converted to string
* @method beforeSet_$field_name
* @param {string} \$value
* @return {array} An array of field name and value
* @throws {Exception} An exception is thrown if \$value is not string or is exceedingly long
*/
EOT;
$js_functions["beforeSet_$field_name"][] = <<<EOT
{$js_null_check}{$js_dbe_check}if (typeof value !== "string" && typeof value !== "number")
throw new Error('Must pass a string to '+this.table()+".$field_name");
if (typeof value === "string" && value.length > $type_display_range)
throw new Error('Exceedingly long value being assigned to '+this.table()+".$field_name");
EOT;
$js_functions["beforeSet_$field_name"]['comment'] = <<<EOT
$dc
* Method is called before setting the field and verifies if value is string of length within acceptable limit.
* Optionally accept numeric value which is converted to string
* @method beforeSet_$field_name
* @param {string} value
* @return {string} The value
* @throws {Error} An exception is thrown if 'value' is not string or is exceedingly long
*/
EOT;
break;
case 'date':
$properties[]="string|Db_Expression $field_name";
$js_properties[] = "$field_name string|Db.Expression";
$functions["beforeSet_$field_name"][] = <<<EOT
{$null_check}{$dbe_check}\$date = date_parse(\$value);
if (!empty(\$date['errors']))
throw new Exception("Date \$value in incorrect format being assigned to ".\$this->getTable().".$field_name");
foreach (array('year', 'month', 'day', 'hour', 'minute', 'second') as \$v)
\$\$v = \$date[\$v];
\$value = sprintf("%04d-%02d-%02d", \$year, \$month, \$day);
EOT;
$functions["beforeSet_$field_name"]['comment'] = <<<EOT
$dc
* Method is called before setting the field and normalize the date string
* @method beforeSet_$field_name
* @param {string} \$value
* @return {array} An array of field name and value
* @throws {Exception} An exception is thrown if \$value does not represent valid date
*/
EOT;
$js_functions["beforeSet_$field_name"][] = <<<EOT
{$js_null_check}{$js_dbe_check}value = new Date(value);
EOT;
$js_functions["beforeSet_$field_name"]['comment'] = <<<EOT
$dc
* Method is called before setting the field
* @method beforeSet_$field_name
* @param {string} value
* @return {Date|Db.Expression} If 'value' is not Db.Expression the current date is returned
*/
EOT;
break;
case 'datetime':
$properties[]="string|Db_Expression $field_name";
$js_properties[] = "$field_name string|Db.Expression";
if (in_array($field_name, array('time_created', 'time_updated', 'created_time', 'updated_time'))) {
$magic_field_names[] = $field_name;
$is_magic_field = true;
}
$functions["beforeSet_$field_name"][] = <<<EOT
{$null_check}{$dbe_check}\$date = date_parse(\$value);
if (!empty(\$date['errors']))
throw new Exception("DateTime \$value in incorrect format being assigned to ".\$this->getTable().".$field_name");
foreach (array('year', 'month', 'day', 'hour', 'minute', 'second') as \$v)
\$\$v = \$date[\$v];
\$value = sprintf("%04d-%02d-%02d %02d:%02d:%02d", \$year, \$month, \$day, \$hour, \$minute, \$second);
EOT;
$functions["beforeSet_$field_name"]['comment'] = <<<EOT
$dc
* Method is called before setting the field and normalize the DateTime string
* @method beforeSet_$field_name
* @param {string} \$value
* @return {array} An array of field name and value
* @throws {Exception} An exception is thrown if \$value does not represent valid DateTime
*/
EOT;
$js_functions["beforeSet_$field_name"][] = <<<EOT
{$js_null_check}{$js_dbe_check}value = new Date(value);
EOT;
$js_functions["beforeSet_$field_name"]['comment'] = <<<EOT
$dc
* Method is called before setting the field
* @method beforeSet_$field_name
* @param {string} value
* @return {Date|Db.Expression} If 'value' is not Db.Expression the current date is returned
*/
EOT;
break;
case 'timestamp':
$properties[]="string $field_name";
$js_properties[] = "$field_name string";
$magic_field_names[] = $field_name;
$is_magic_field = true;
break;
case 'decimal':
$properties[]="float $field_name";
$js_properties[] = "$field_name number";
$js_functions["beforeSet_$field_name"][] = <<<EOT
{$js_null_check}{$js_dbe_check}value = Number(value);
if (isNaN(value))
throw new Error('Non-number value being assigned to '+this.table()+".$field_name");
EOT;
$js_functions["beforeSet_$field_name"]['comment'] = <<<EOT
$dc
* Method is called before setting the field to verify if value is a number
* @method beforeSet_$field_name
* @param {integer} value
* @return {integer} The value
* @throws {Error} If 'value' is not number
*/
EOT;
break;
default:
$properties[]="mixed $field_name";
$js_properties[] = "$field_name mixed";
break;
}
if (! empty($functions["beforeSet_$field_name"])) {
$functions["beforeSet_$field_name"]['return_statement'] = <<<EOT
return array('$field_name', \$value);
EOT;
}
if (! empty($js_functions["beforeSet_$field_name"])) {
$js_functions["beforeSet_$field_name"]['return_statement'] = <<<EOT
return value;
EOT;
}
if (! $field_null and ! $is_magic_field and ! $auto_inc
//and (in_array($type_name, array(
// 'tinyint', 'smallint', 'mediumint', 'int', 'bigint', 'enum',
//))
and !isset($field_default)
//)
) {
$required_field_names[] = "'$field_name'";
}
}
$field_names_json = json_encode($field_names);
$field_names_json_indented = str_replace(
array("[", ",", "]"),
array("[\n\t\t", ",\n\t\t", "\n\t]"),
$field_names_json
);
$functions['afterSet'] = array();
$field_names_exported = "\$this->fieldNames()";
$afterSet_code = <<<EOT
if (!in_array(\$name, $field_names_exported))
\$this->notModified(\$name);
EOT;
$return_statement = <<<EOT
return \$value;
EOT;
$functions["afterSet"][] = $afterSet_code;
$functions['afterSet']['return_statement'] = $return_statement;
$functions['afterSet']['args'] = '$name, $value';
$functions['afterSet']['comment'] = <<<EOT
$dc
* Method is called after field is set and used to keep \$fields_modified property up to date
* @method afterSet
* @param {string} \$name The field name
* @param {mixed} \$value The value of the field
* @return {mixed} Original value
*/
EOT;
$functions['beforeSave'] = array();
$js_functions['beforeSave'] = array();
if ($required_field_names) {
$required_fields_string = implode(',', $required_field_names);
$beforeSave_code = <<<EOT
if (!\$this->retrieved) {
\$table = \$this->getTable();
foreach (array($required_fields_string) as \$name) {
if (!isset(\$value[\$name])) {
throw new Exception("the field \$table.\$name needs a value, because it is NOT NULL, not auto_increment, and lacks a default value.");
}
}
}
EOT;
$js_beforeSave_code = <<<EOT
var fields = [$required_fields_string], i;
if (!this._retrieved) {
var table = this.table();
for (i=0; i<fields.length; i++) {
if (typeof this.fields[fields[i]] === "undefined") {
throw new Error("the field "+table+"."+fields[i]+" needs a value, because it is NOT NULL, not auto_increment, and lacks a default value.");
}
}
}
EOT;
$return_statement = <<<EOT
return \$value;
EOT;
$js_return_statement = <<<EOT
return value;
EOT;
$functions["beforeSave"][] = $beforeSave_code;
$functions['beforeSave']['return_statement'] = $return_statement;
$functions['beforeSave']['comment'] = <<<EOT
$dc
* Check if mandatory fields are set and updates 'magic fields' with appropriate values
* @method beforeSave
* @param {array} \$value The array of fields
* @return {array}
* @throws {Exception} If mandatory field is not set
*/
EOT;
$js_functions["beforeSave"][] = $js_beforeSave_code;
$js_functions['beforeSave']['return_statement'] = $js_return_statement;
$js_functions['beforeSave']['comment'] = <<<EOT
$dc
* Check if mandatory fields are set and updates 'magic fields' with appropriate values
* @method beforeSave
* @param {array} value The array of fields
* @return {array}
* @throws {Error} If mandatory field is not set
*/
EOT;
}
//$functions['beforeSave'] = array();
if (count($magic_field_names) > 0) {
$beforeSave_code = '';
$js_beforeSave_code = '';
foreach (array('time_created', 'created_time') as $cmf) {
if (in_array($cmf, $magic_field_names)) {
$beforeSave_code .= <<<EOT
if (!\$this->retrieved and !isset(\$value['$cmf']))
\$value['$cmf'] = new Db_Expression('CURRENT_TIMESTAMP');
EOT;
$js_beforeSave_code .= <<<EOT
if (!this._retrieved && !value['$cmf'])
value['$cmf'] = new Db.Expression('CURRENT_TIMESTAMP');
EOT;
break;
}
}
foreach (array('time_updated', 'updated_time') as $umf) {
if (in_array($umf, $magic_field_names)) {
$beforeSave_code .= <<<EOT
//if (\$this->retrieved and !isset(\$value['$umf']))
// convention: we'll have $umf = $cmf if just created.
\$value['$umf'] = new Db_Expression('CURRENT_TIMESTAMP');
EOT;
$js_beforeSave_code .= <<<EOT
// convention: we'll have $umf = $cmf if just created.
value['$umf'] = new Db.Expression('CURRENT_TIMESTAMP');
EOT;
break;
}
}
$return_statement = <<<EOT
return \$value;
EOT;
$js_return_statement = <<<EOT
return value;
EOT;
$functions['beforeSave'][] = $beforeSave_code;
$functions['beforeSave']['return_statement'] = $return_statement;
$js_functions['beforeSave'][] = $js_beforeSave_code;
$js_functions['beforeSave']['return_statement'] = $js_return_statement;
}
$functions['fieldNames'] = array();
$fieldNames_exported = Db_Utils::var_export($field_names);
$fieldNames_code = <<<EOT
\$field_names = $fieldNames_exported;
\$result = \$field_names;
if (!empty(\$table_alias)) {
\$temp = array();
foreach (\$result as \$field_name)
\$temp[] = \$table_alias . '.' . \$field_name;
\$result = \$temp;
}
if (!empty(\$field_alias_prefix)) {
\$temp = array();
reset(\$field_names);
foreach (\$result as \$field_name) {
\$temp[\$field_alias_prefix . current(\$field_names)] = \$field_name;
next(\$field_names);
}
\$result = \$temp;
}
EOT;
$return_statement = <<<EOT
return \$result;
EOT;
$functions['fieldNames'][] = $fieldNames_code;
$functions['fieldNames']['return_statement'] = $return_statement;
$functions['fieldNames']['args'] = '$table_alias = null, $field_alias_prefix = null';
$functions['fieldNames']['modifiers'] = 'static';
$functions['fieldNames']['comment'] = <<<EOT
$dc
* Retrieves field names for class table
* @method fieldNames
* @static
* @param {string} [\$table_alias=null] If set, the alieas is added to each field
* @param {string} [\$field_alias_prefix=null] If set, the method returns associative array of `'prefixed field' => 'field'` pairs
* @return {array} An array of field names
*/
EOT;
$functions_code = array();
foreach ($functions as $func_name => $func_code) {
$func_args = isset($func_code['args']) ? $func_code['args'] : '$value';
$func_modifiers = isset($func_code['modifiers']) ? $func_code['modifiers'].' ' : '';
$func_code_string = isset($func_code['comment']) ? $func_code['comment']."\n" : '';
$func_code_string .= <<<EOT
{$func_modifiers}function $func_name($func_args)
{
EOT;
if (is_array($func_code) and ! empty($func_code)) {
foreach ($func_code as $key => $code_tool) {
if (is_string($key))
continue;
$func_code_string .= $code_tool;
}
$func_code_string .= "\n" . $func_code['return_statement'];
}
$func_code_string .= <<<EOT
}
EOT;
if (! empty($func_code))
$functions_code[] = $func_code_string;
}
$functions_string = implode("\n\n", $functions_code);
foreach ($js_functions as $func_name => $func_code) {
$func_args = isset($func_code['args']) ? $func_code['args'] : 'value';
$instance = isset($func_code['instance']) ? '.prototype' : '';
$func_code_string = isset($func_code['comment']) ? $func_code['comment']."\n" : '';
$func_code_string .= <<<EOT
Base.prototype.$func_name = function ($func_args) {
EOT;
if (is_array($func_code) and ! empty($func_code)) {
foreach ($func_code as $key => $code_tool) {
if (is_string($key))
continue;
$func_code_string .= $code_tool;
}
$func_code_string .= "\n" . $func_code['return_statement'];
}
$func_code_string .= <<<EOT
};
EOT;
if (! empty($func_code))
$js_functions_code[] = $func_code_string;
}
$js_functions_string = implode("\n\n", $js_functions_code);
$pk_exported_indented = str_replace("\n", "\n\t\t\t", $pk_exported);
$pk_json_indented = str_replace(
array("[", ",", "]"),
array("[\n\t\t", ",\n\t\t", "\n\t]"),
$pk_json
);
$conn_name_var = var_export($conn_name, true);
$class_name_var = var_export($class_name, true);
$class_name_prefix = rtrim(ucfirst($classname_prefix), "._");
foreach ($properties as $k => $v) {
$tmp = explode(' ', $v);
$properties[$k] = <<<EOT
$dc
* @property \${$tmp[1]}
* @type $tmp[0]
*/
EOT;
}
foreach ($js_properties as $k => $v) {
$tmp = explode(' ', $v);
$js_properties[$k] = <<<EOT
$dc
* @property $tmp[0]
* @type $tmp[1]
*/
EOT;
}
$field_hints = implode("\n", $properties);
$js_field_hints = implode("\n", $js_properties);
// Here is the base class:
$base_class_string = <<<EOT
<?php
$dc
* Autogenerated base class representing $table_name_base rows
* in the $conn_name database.
*
* Don't change this file, since it can be overwritten.
* Instead, change the $class_name.php file.
*
* @module $conn_name
*/
$dc
* Base class representing '$class_name_base' rows in the '$conn_name' database
* @class Base_$class_name
* @extends Db_Row
*/
abstract class Base_$class_name extends Db_Row
{
$field_hints
$dc
* The setUp() method is called the first time
* an object of this class is constructed.
* @method setUp
*/
function setUp()
{
\$this->setDb(self::db());
\$this->setTable(self::table());
\$this->setPrimaryKey(
$pk_exported_indented
);
}
$dc
* Connects to database
* @method db
* @static
* @return {iDb} The database object
*/
static function db()
{
return Db::connect($conn_name_var);
}
$dc
* Retrieve the table name to use in SQL statement
* @method table
* @static
* @param {boolean} [\$with_db_name=true] Indicates wheather table name shall contain the database name
* @return {string|Db_Expression} The table name as string optionally without database name if no table sharding
* was started or Db_Expression class with prefix and database name templates is table was sharded
*/
static function table(\$with_db_name = true)
{
if (Q_Config::get('Db', 'connections', '$conn_name', 'indexes', '$class_name_base', false)) {
return new Db_Expression((\$with_db_name ? '{\$dbname}.' : '').'{\$prefix}'.'$table_name_base');
} else {
\$conn = Db::getConnection($conn_name_var);
\$prefix = empty(\$conn['prefix']) ? '' : \$conn['prefix'];
\$table_name = \$prefix . '$table_name_base';
if (!\$with_db_name)
return \$table_name;
\$db = Db::connect($conn_name_var);
return \$db->dbName().'.'.\$table_name;
}
}
$dc
* The connection name for the class
* @method connectionName
* @static
* @return {string} The name of the connection
*/
static function connectionName()
{
return $conn_name_var;
}
$dc
* Create SELECT query to the class table
* @method select
* @static
* @param \$fields {array} The field values to use in WHERE clauseas as
* an associative array of `column => value` pairs
* @param [\$alias=null] {string} Table alias
* @return {Db_Query_Mysql} The generated query
*/
static function select(\$fields, \$alias = null)
{
if (!isset(\$alias)) \$alias = '';
\$q = self::db()->select(\$fields, self::table().' '.\$alias);
\$q->className = $class_name_var;
return \$q;
}
$dc
* Create UPDATE query to the class table
* @method update
* @static
* @param [\$alias=null] {string} Table alias
* @return {Db_Query_Mysql} The generated query
*/
static function update(\$alias = null)
{
if (!isset(\$alias)) \$alias = '';
\$q = self::db()->update(self::table().' '.\$alias);
\$q->className = $class_name_var;
return \$q;
}
$dc
* Create DELETE query to the class table
* @method delete
* @static
* @param [\$table_using=null] {object} If set, adds a USING clause with this table
* @param [\$alias=null] {string} Table alias
* @return {Db_Query_Mysql} The generated query
*/
static function delete(\$table_using = null, \$alias = null)
{
if (!isset(\$alias)) \$alias = '';
\$q = self::db()->delete(self::table().' '.\$alias, \$table_using);
\$q->className = $class_name_var;
return \$q;
}
$dc
* Create INSERT query to the class table
* @method insert
* @static
* @param [\$fields=array()] {object} The fields as an associative array of `column => value` pairs
* @param [\$alias=null] {string} Table alias
* @return {Db_Query_Mysql} The generated query
*/
static function insert(\$fields = array(), \$alias = null)
{
if (!isset(\$alias)) \$alias = '';
\$q = self::db()->insert(self::table().' '.\$alias, \$fields);
\$q->className = $class_name_var;
return \$q;
}
$dc
* Inserts multiple records into a single table, preparing the statement only once,
* and executes all the queries.
* @method insertManyAndExecute
* @static
* @param {array} [\$records=array()] The array of records to insert.
* (The field names for the prepared statement are taken from the first record.)
* You cannot use Db_Expression objects here, because the function binds all parameters with PDO.
* @param {array} [\$options=array()]
* An associative array of options, including:
*
* * "chunkSize" {integer} The number of rows to insert at a time. Defaults to 1.<br/>
* * "onDuplicateKeyUpdate" {array} You can put an array of fieldname => value pairs here,
* which will add an ON DUPLICATE KEY UPDATE clause to the query.
*
*/
static function insertManyAndExecute(\$records = array(), \$options = array())
{
self::db()->insertManyAndExecute(self::table(), \$records, \$options);
}
$functions_string
};
EOT;
// Set the JS code
$js_code = <<<EOT
$dc
* Autogenerated base class representing $table_name_base rows
* in the $conn_name database.
*
* Don't change this file, since it can be overwritten.
* Instead, change the $class_name_prefix/$class_name_base.js file.
*
* @module $conn_name
*/
var Q = require('Q');
var Db = Q.require('Db');
var $conn_name = Q.require('$conn_name');
$dc
* Base class representing '$class_name_base' rows in the '$conn_name' database
* @namespace Base.$class_name_prefix
* @class $class_name_base
* @extends Db.Row
* @constructor
* @param {object} [fields={}] The fields values to initialize table row as
* an associative array of `{column: value}` pairs
*/
function Base (fields) {
$dc
* The name of the class
* @property className
* @type string
*/
this.className = "$class_name";
}
Q.mixin(Base, Q.require('Db/Row'));
$js_field_hints
$dc
* This method uses Db to establish a connection with the information stored in the configuration.
* If the this Db object has already been made, it returns this Db object.
* @method db
* @return {Db} The database connection
*/
Base.db = function () {
return $conn_name.db();
};
$dc
* Retrieve the table name to use in SQL statement
* @method table
* @param [withoutDbName=false] {boolean} Indicates wheather table name shall contain the database name
* @return {string|Db.Expression} The table name as string optionally without database name if no table sharding was started
* or Db.Expression object with prefix and database name templates is table was sharded
*/
Base.table = function (withoutDbName) {
if (Q.Config.get(['Db', 'connections', '$conn_name', 'indexes', '$class_name_base'], false)) {
return new Db.Expression((withoutDbName ? '' : '{\$dbname}.')+'{\$prefix}$table_name_base');
} else {
var conn = Db.getConnection('$conn_name');
var prefix = conn.prefix || '';
var tableName = prefix + '$table_name_base';
var dbname = Base.table.dbname;
if (!dbname) {
var dsn = Db.parseDsnString(conn['dsn']);
dbname = Base.table.dbname = dsn.dbname;
}
return withoutDbName ? tableName : dbname + '.' + tableName;
}
};
$dc
* The connection name for the class
* @method connectionName
* @return {string} The name of the connection
*/
Base.connectionName = function() {
return '$conn_name';
};
$dc
* Create SELECT query to the class table
* @method SELECT
* @param fields {object|string} The field values to use in WHERE clauseas as an associative array of `{column: value}` pairs
* @param [alias=null] {string} Table alias
* @return {Db.Query.Mysql} The generated query
*/
Base.SELECT = function(fields, alias) {
var q = Base.db().SELECT(fields, Base.table()+(alias ? ' '+alias : ''));
q.className = '$class_name';
return q;
};
$dc
* Create UPDATE query to the class table. Use Db.Query.Mysql.set() method to define SET clause
* @method UPDATE
* @param [alias=null] {string} Table alias
* @return {Db.Query.Mysql} The generated query
*/
Base.UPDATE = function(alias) {
var q = Base.db().UPDATE(Base.table()+(alias ? ' '+alias : ''));
q.className = '$class_name';
return q;
};
$dc
* Create DELETE query to the class table
* @method DELETE
* @param [table_using=null] {object} If set, adds a USING clause with this table
* @param [alias=null] {string} Table alias
* @return {Db.Query.Mysql} The generated query
*/
Base.DELETE = function(table_using, alias) {
var q = Base.db().DELETE(Base.table()+(alias ? ' '+alias : ''), table_using);
q.className = '$class_name';
return q;
};
$dc
* Create INSERT query to the class table
* @method INSERT
* @param {object} [fields={}] The fields as an associative array of `{column: value}` pairs
* @param [alias=null] {string} Table alias
* @return {Db.Query.Mysql} The generated query
*/
Base.INSERT = function(fields, alias) {
var q = Base.db().INSERT(Base.table()+(alias ? ' '+alias : ''), fields || {});
q.className = '$class_name';
return q;
};
// Instance methods
Base.prototype.setUp = function() {
// does nothing for now
};
Base.prototype.db = function () {
return Base.db();
};
Base.prototype.table = function () {
return Base.table();
};
$dc
* Retrieves primary key fields names for class table
* @method primaryKey
* @return {string[]} An array of field names
*/
Base.prototype.primaryKey = function () {
return $pk_json_indented;
};
$dc
* Retrieves field names for class table
* @method fieldNames
* @return {array} An array of field names
*/
Base.prototype.fieldNames = function () {
return $field_names_json_indented;
};
$js_functions_string
module.exports = Base;
EOT;
// Return the base class
return $base_class_string; // unless the above line threw an exception
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment