Требование: PHP => 5.3
[cswowd.git] / include / DbSimple / Generic.php
blobac2a036c3f8e7e846dbfd41ea6c35833988e0a56
1 <?php
2 /**
3 * DbSimple_Generic: universal database connected by DSN.
4 * (C) Dk Lab, http://en.dklab.ru
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
10 * See http://www.gnu.org/copyleft/lesser.html
12 * Use static DbSimple_Generic::connect($dsn) call if you don't know
13 * database type and parameters, but have its DSN.
15 * Additional keys can be added by appending a URI query string to the
16 * end of the DSN.
18 * The format of the supplied DSN is in its fullest form:
19 * phptype(dbsyntax)://username:password@protocol+hostspec/database?option=8&another=true
21 * Most variations are allowed:
22 * phptype://username:password@protocol+hostspec:110//usr/db_file.db?mode=0644
23 * phptype://username:password@hostspec/database_name
24 * phptype://username:password@hostspec
25 * phptype://username@hostspec
26 * phptype://hostspec/database
27 * phptype://hostspec
28 * phptype(dbsyntax)
29 * phptype
31 * Parsing code is partially grabbed from PEAR DB class,
32 * initial author: Tomas V.V.Cox <cox@idecnet.com>.
34 * Contains 3 classes:
35 * - DbSimple_Generic: database factory class
36 * - DbSimple_Generic_Database: common database methods
37 * - DbSimple_Generic_Blob: common BLOB support
38 * - DbSimple_Generic_LastError: error reporting and tracking
40 * Special result-set fields:
41 * - ARRAY_KEY* ("*" means "anything")
42 * - PARENT_KEY
44 * Transforms:
45 * - GET_ATTRIBUTES
46 * - CALC_TOTAL
47 * - GET_TOTAL
48 * - UNIQ_KEY
50 * Query attributes:
51 * - BLOB_OBJ
52 * - CACHE
54 * @author Dmitry Koterov, http://forum.dklab.ru/users/DmitryKoterov/
55 * @author Konstantin Zhinko, http://forum.dklab.ru/users/KonstantinGinkoTit/
57 * @version 2.x $Id: Generic.php 226 2007-09-17 21:00:15Z dk $
60 /**
61 * Use this constant as placeholder value to skip optional SQL block [...].
63 define('DBSIMPLE_SKIP', log(0));
65 /**
66 * Names of special columns in result-set which is used
67 * as array key (or karent key in forest-based resultsets) in
68 * resulting hash.
70 define('DBSIMPLE_ARRAY_KEY', 'ARRAY_KEY'); // hash-based resultset support
71 define('DBSIMPLE_PARENT_KEY', 'PARENT_KEY'); // forrest-based resultset support
74 /**
75 * DbSimple factory.
77 class DbSimple_Generic
79 /**
80 * DbSimple_Generic connect(mixed $dsn)
82 * Universal static function to connect ANY database using DSN syntax.
83 * Choose database driver according to DSN. Return new instance
84 * of this driver.
86 function& connect($dsn)
88 // Load database driver and create its instance.
89 $parsed = DbSimple_Generic::parseDSN($dsn);
90 if (!$parsed) {
91 $dummy = null;
92 return $dummy;
94 $class = 'DbSimple_'.ucfirst($parsed['scheme']);
95 if (!class_exists($class)) {
96 $file = str_replace('_', '/', $class) . ".php";
97 // Try to load library file from standard include_path.
98 if ($f = @fopen($file, "r", true)) {
99 fclose($f);
100 require_once($file);
101 } else {
102 // Wrong include_path; try to load from current directory.
103 $base = basename($file);
104 $dir = dirname(__FILE__);
105 if (@is_file($path = "$dir/$base")) {
106 require_once($path);
107 } else {
108 trigger_error("Error loading database driver: no file $file in include_path; no file $base in $dir", E_USER_ERROR);
109 return null;
113 $object = new $class($parsed);
114 if (isset($parsed['ident_prefix'])) {
115 $object->setIdentPrefix($parsed['ident_prefix']);
117 $object->setCachePrefix(md5(serialize($parsed['dsn'])));
118 if (@fopen('Cache/Lite.php', 'r', true)) {
119 $tmp_dirs = array(
120 ini_get('session.save_path'),
121 getenv("TEMP"),
122 getenv("TMP"),
123 getenv("TMPDIR"),
124 '/tmp'
126 foreach ($tmp_dirs as $dir) {
127 if (!$dir) continue;
128 $fp = @fopen($testFile = $dir . '/DbSimple_' . md5(getmypid() . microtime()), 'w');
129 if ($fp) {
130 fclose($fp);
131 unlink($testFile);
132 require_once 'Cache' . '/Lite.php'; // "." -> no phpEclipse notice
133 $t = new Cache_Lite(array('cacheDir' => $dir.'/', 'lifeTime' => null, 'automaticSerialization' => true));
134 $object->_cacher =& $t;
135 break;
140 return $object;
145 * array parseDSN(mixed $dsn)
146 * Parse a data source name.
147 * See parse_url() for details.
149 function parseDSN($dsn)
151 if (is_array($dsn)) return $dsn;
152 $parsed = @parse_url($dsn);
153 if (!$parsed) return null;
154 $params = null;
155 if (!empty($parsed['query'])) {
156 parse_str($parsed['query'], $params);
157 $parsed += $params;
159 $parsed['dsn'] = $dsn;
160 return $parsed;
166 * Base class for all databases.
167 * Can create transactions and new BLOBs, parse DSNs.
169 * Logger is COMMON for multiple transactions.
170 * Error handler is private for each transaction and database.
172 class DbSimple_Generic_Database extends DbSimple_Generic_LastError
175 * Public methods.
179 * object blob($blob_id)
180 * Create new blob
182 function blob($blob_id = null)
184 $this->_resetLastError();
185 return $this->_performNewBlob($blob_id);
189 * void transaction($mode)
190 * Create new transaction.
192 function transaction($mode=null)
194 $this->_resetLastError();
195 $this->_logQuery('-- START TRANSACTION '.$mode);
196 return $this->_performTransaction($mode);
200 * mixed commit()
201 * Commit the transaction.
203 function commit()
205 $this->_resetLastError();
206 $this->_logQuery('-- COMMIT');
207 return $this->_performCommit();
211 * mixed rollback()
212 * Rollback the transaction.
214 function rollback()
216 $this->_resetLastError();
217 $this->_logQuery('-- ROLLBACK');
218 return $this->_performRollback();
222 * mixed select(string $query [, $arg1] [,$arg2] ...)
223 * Execute query and return the result.
225 function select($query)
227 $args = func_get_args();
228 $total = false;
229 return $this->_query($args, $total);
233 * mixed selectPage(int &$total, string $query [, $arg1] [,$arg2] ...)
234 * Execute query and return the result.
235 * Total number of found rows (independent to LIMIT) is returned in $total
236 * (in most cases second query is performed to calculate $total).
238 function selectPage(&$total, $query)
240 $args = func_get_args();
241 array_shift($args);
242 $total = true;
243 return $this->_query($args, $total);
247 * hash selectRow(string $query [, $arg1] [,$arg2] ...)
248 * Return the first row of query result.
249 * On errors return null and set last error.
250 * If no one row found, return array()! It is useful while debugging,
251 * because PHP DOES NOT generates notice on $row['abc'] if $row === null
252 * or $row === false (but, if $row is empty array, notice is generated).
254 function selectRow()
256 $args = func_get_args();
257 $total = false;
258 $rows = $this->_query($args, $total);
259 if (!is_array($rows)) return $rows;
260 if (!count($rows)) return array();
261 reset($rows);
262 return current($rows);
266 * array selectCol(string $query [, $arg1] [,$arg2] ...)
267 * Return the first column of query result as array.
269 function selectCol()
271 $args = func_get_args();
272 $total = false;
273 $rows = $this->_query($args, $total);
274 if (!is_array($rows)) return $rows;
275 $this->_shrinkLastArrayDimensionCallback($rows);
276 return $rows;
280 * scalar selectCell(string $query [, $arg1] [,$arg2] ...)
281 * Return the first cell of the first column of query result.
282 * If no one row selected, return null.
284 function selectCell()
286 $args = func_get_args();
287 $total = false;
288 $rows = $this->_query($args, $total);
289 if (!is_array($rows)) return $rows;
290 if (!count($rows)) return null;
291 reset($rows);
292 $row = current($rows);
293 if (!is_array($row)) return $row;
294 reset($row);
295 return current($row);
299 * mixed query(string $query [, $arg1] [,$arg2] ...)
300 * Alias for select(). May be used for INSERT or UPDATE queries.
302 function query()
304 $args = func_get_args();
305 $total = false;
306 return $this->_query($args, $total);
310 * string escape(mixed $s, bool $isIdent=false)
311 * Enclose the string into database quotes correctly escaping
312 * special characters. If $isIdent is true, value quoted as identifier
313 * (e.g.: `value` in MySQL, "value" in Firebird, [value] in MSSQL).
315 function escape($s, $isIdent=false)
317 return $this->_performEscape($s, $isIdent);
322 * callback setLogger(callback $logger)
323 * Set query logger called before each query is executed.
324 * Returns previous logger.
326 function setLogger($logger)
328 $prev = $this->_logger;
329 $this->_logger = $logger;
330 return $prev;
334 * callback setCacher(callback $cacher)
335 * Set cache mechanism called during each query if specified.
336 * Returns previous handler.
338 function setCacher($cacher)
340 $prev = $this->_cacher;
341 $this->_cacher = $cacher;
342 return $prev;
346 * string setIdentPrefix($prx)
347 * Set identifier prefix used for $_ placeholder.
349 function setIdentPrefix($prx)
351 $old = $this->_identPrefix;
352 if ($prx !== null) $this->_identPrefix = $prx;
353 return $old;
357 * string setIdentPrefix($prx)
358 * Set cache prefix used in key caclulation.
360 function setCachePrefix($prx)
362 $old = $this->_cachePrefix;
363 if ($prx !== null) $this->_cachePrefix = $prx;
364 return $old;
368 * array getStatistics()
369 * Returns various statistical information.
371 function getStatistics()
373 return $this->_statistics;
378 * Virtual protected methods
380 function ____________PROTECTED() {} // for phpEclipse outline
384 * string _performEscape(mixed $s, bool $isIdent=false)
386 function _performEscape($s, $isIdent)
388 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
392 * object _performNewBlob($id)
394 * Returns new blob object.
396 function& _performNewBlob($id)
398 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
402 * list _performGetBlobFieldNames($resultResource)
403 * Get list of all BLOB field names in result-set.
405 function _performGetBlobFieldNames($result)
407 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
411 * mixed _performTransformQuery(array &$query, string $how)
413 * Transform query different way specified by $how.
414 * May return some information about performed transform.
416 function _performTransformQuery(&$queryMain, $how)
418 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
423 * resource _performQuery($arrayQuery)
424 * Must return:
425 * - For SELECT queries: ID of result-set (PHP resource).
426 * - For other queries: query status (scalar).
427 * - For error queries: null (and call _setLastError()).
429 function _performQuery($arrayQuery)
431 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
435 * mixed _performFetch($resultResource)
436 * Fetch ONE NEXT row from result-set.
437 * Must return:
438 * - For SELECT queries: all the rows of the query (2d arrray).
439 * - For INSERT queries: ID of inserted row.
440 * - For UPDATE queries: number of updated rows.
441 * - For other queries: query status (scalar).
442 * - For error queries: null (and call _setLastError()).
444 function _performFetch($result)
446 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
450 * array _performTotal($arrayQuery)
452 function _performTotal($arrayQuery)
454 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
458 * mixed _performTransaction($mode)
459 * Start new transaction.
461 function _performTransaction($mode=null)
463 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
467 * mixed _performCommit()
468 * Commit the transaction.
470 function _performCommit()
472 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
476 * mixed _performRollback()
477 * Rollback the transaction.
479 function _performRollback()
481 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
485 * string _performGetPlaceholderIgnoreRe()
486 * Return regular expression which matches ignored query parts.
487 * This is needed to skip placeholder replacement inside comments, constants etc.
489 function _performGetPlaceholderIgnoreRe()
491 return '';
495 * Returns marker for native database placeholder. E.g. in FireBird it is '?',
496 * in PostgreSQL - '$1', '$2' etc.
498 * @param int $n Number of native placeholder from the beginning of the query (begins from 0!).
499 * @return string String representation of native placeholder marker (by default - '?').
501 function _performGetNativePlaceholderMarker($n)
503 return '?';
508 * Private methods.
510 function ____________PRIVATE() {} // for phpEclipse outline
514 * array _query($query, &$total)
515 * See _performQuery().
517 function _query($query, &$total)
519 $this->_resetLastError();
521 // Fetch query attributes.
522 $this->attributes = $this->_transformQuery($query, 'GET_ATTRIBUTES');
524 // Modify query if needed for total counting.
525 if ($total) {
526 $this->_transformQuery($query, 'CALC_TOTAL');
528 $is_cacher_callable = (is_callable($this->_cacher) || (method_exists($this->_cacher, 'get') && method_exists($this->_cacher, 'save')));
529 $rows = null;
530 $cache_it = false;
531 if (!empty($this->attributes['CACHE']) && $is_cacher_callable) {
533 $hash = $this->_cachePrefix . md5(serialize($query));
534 // Getting data from cache if possible
535 $fetchTime = $firstFetchTime = 0;
536 $qStart = $this->_microtime();
537 $cacheData = $this->_cache($hash);
538 $queryTime = $this->_microtime() - $qStart;
540 $storeTime = isset($cacheData['storeTime']) ? $cacheData['storeTime'] : null;
541 $invalCache = isset($cacheData['invalCache']) ? $cacheData['invalCache'] : null;
542 $result = isset($cacheData['result']) ? $cacheData['result'] : null;
543 $rows = isset($cacheData['rows']) ? $cacheData['rows'] : null;
546 $cache_params = $this->attributes['CACHE'];
548 // Calculating cache time to live
549 $re = '/
551 ([0-9]+) #2 - hours
552 h)? [ \t]*
554 ([0-9]+) #4 - minutes
555 m)? [ \t]*
557 ([0-9]+) #6 - seconds
558 s?)? (,)?
559 /sx';
560 $m = null;
561 preg_match($re, $cache_params, $m);
562 $ttl = @$m[6] + @$m[4] * 60 + @$m[2] * 3600;
563 // Cutting out time param - now there are just fields for uniqKey or nothing
564 $cache_params = trim(preg_replace($re, '', $cache_params, 1));
566 $uniq_key = null;
568 // UNIQ_KEY calculation
569 if (!empty($cache_params)) {
570 $dummy = null;
571 // There is no need in query, cos' needle in $this->attributes['CACHE']
572 $this->_transformQuery($dummy, 'UNIQ_KEY');
573 $uniq_key = call_user_func_array(array(&$this, 'select'), $dummy);
574 $uniq_key = md5(serialize($uniq_key));
576 // Check TTL?
577 $ttl = empty($ttl) ? true : (int)$storeTime > (time() - $ttl);
579 // Invalidate cache?
580 if ($ttl && $uniq_key == $invalCache) {
581 $this->_logQuery($query);
582 $this->_logQueryStat($queryTime, $fetchTime, $firstFetchTime, $rows, true);
585 else $cache_it = true;
588 if (null === $rows || true === $cache_it) {
589 $this->_logQuery($query);
591 // Run the query (counting time).
592 $qStart = $this->_microtime();
593 $result = $this->_performQuery($query);
594 $fetchTime = $firstFetchTime = 0;
596 if (is_resource($result)) {
597 $rows = array();
598 // Fetch result row by row.
599 $fStart = $this->_microtime();
600 $row = $this->_performFetch($result);
601 $firstFetchTime = $this->_microtime() - $fStart;
602 if ($row !== null) {
603 $rows[] = $row;
604 while ($row=$this->_performFetch($result)) {
605 $rows[] = $row;
608 $fetchTime = $this->_microtime() - $fStart;
609 } else {
610 $rows = $result;
612 $queryTime = $this->_microtime() - $qStart;
614 // Log query statistics.
615 $this->_logQueryStat($queryTime, $fetchTime, $firstFetchTime, $rows, false);
617 // Prepare BLOB objects if needed.
618 if (is_array($rows) && !empty($this->attributes['BLOB_OBJ'])) {
619 $blobFieldNames = $this->_performGetBlobFieldNames($result);
620 foreach ($blobFieldNames as $name) {
621 for ($r = count($rows)-1; $r>=0; $r--) {
622 $rows[$r][$name] =& $this->_performNewBlob($rows[$r][$name]);
627 // Transform resulting rows.
628 $result = $this->_transformResult($rows);
630 // Storing data in cache
631 if ($cache_it && $is_cacher_callable) {
632 $this->_cache(
633 $hash,
634 array(
635 'storeTime' => time(),
636 'invalCache' => $uniq_key,
637 'result' => $result,
638 'rows' => $rows
644 // Count total number of rows if needed.
645 if (is_array($result) && $total) {
646 $this->_transformQuery($query, 'GET_TOTAL');
647 $total = call_user_func_array(array(&$this, 'selectCell'), $query);
650 return $result;
655 * mixed _transformQuery(array &$query, string $how)
657 * Transform query different way specified by $how.
658 * May return some information about performed transform.
660 function _transformQuery(&$query, $how)
662 // Do overriden transformation.
663 $result = $this->_performTransformQuery($query, $how);
664 if ($result === true) return $result;
665 // Common transformations.
666 switch ($how) {
667 case 'GET_ATTRIBUTES':
668 // Extract query attributes.
669 $options = array();
670 $q = $query[0];
671 $m = null;
672 while (preg_match('/^ \s* -- [ \t]+ (\w+): ([^\r\n]+) [\r\n]* /sx', $q, $m)) {
673 $options[$m[1]] = trim($m[2]);
674 $q = substr($q, strlen($m[0]));
676 return $options;
677 case 'UNIQ_KEY':
678 $q = $this->attributes['CACHE'];
679 $i = 0;
680 $query = " -- UNIQ_KEY\n";
681 while(preg_match('/(\w+)\.\w+/sx', $q, $m)) {
682 if($i > 0)$query .= "\nUNION\n";
683 $query .= 'SELECT MAX('.$m[0].') AS M, COUNT(*) AS C FROM '.$m[1];
684 $q = substr($q, strlen($m[0]));
685 $i++;
687 return true;
689 // No such transform.
690 $this->_setLastError(-1, "No such transform type: $how", $query);
695 * void _expandPlaceholders(array &$queryAndArgs, bool $useNative=false)
696 * Replace placeholders by quoted values.
697 * Modify $queryAndArgs.
699 function _expandPlaceholders(&$queryAndArgs, $useNative=false)
701 $cacheCode = null;
702 if ($this->_logger) {
703 // Serialize is much faster than placeholder expansion. So use caching.
704 $cacheCode = md5(serialize($queryAndArgs) . '|' . $useNative . '|' . $this->_identPrefix);
705 if (isset($this->_placeholderCache[$cacheCode])) {
706 $queryAndArgs = $this->_placeholderCache[$cacheCode];
707 return;
711 if (!is_array($queryAndArgs)) {
712 $queryAndArgs = array($queryAndArgs);
715 $this->_placeholderNativeArgs = $useNative? array() : null;
716 $this->_placeholderArgs = array_reverse($queryAndArgs);
718 $query = array_pop($this->_placeholderArgs); // array_pop is faster than array_shift
720 // Do all the work.
721 $this->_placeholderNoValueFound = false;
722 $query = $this->_expandPlaceholdersFlow($query);
724 if ($useNative) {
725 array_unshift($this->_placeholderNativeArgs, $query);
726 $queryAndArgs = $this->_placeholderNativeArgs;
727 } else {
728 $queryAndArgs = array($query);
731 if ($cacheCode) {
732 $this->_placeholderCache[$cacheCode] = $queryAndArgs;
738 * Do real placeholder processing.
739 * Imply that all interval variables (_placeholder_*) already prepared.
740 * May be called recurrent!
742 function _expandPlaceholdersFlow($query)
744 $re = '{
746 # Ignored chunks.
748 # Comment.
749 -- [^\r\n]*
752 (?>
753 # DB-specifics.
754 ' . trim($this->_performGetPlaceholderIgnoreRe()) . '
758 (?>
759 # Optional blocks
761 # Use "+" here, not "*"! Else nested blocks are not processed well.
762 ( (?> (?>[^{}]+) | (?R) )* ) #1
767 # Placeholder
768 (\?) ( [_dsafn\#]? ) #2 #3
770 }sx';
771 $query = preg_replace_callback(
772 $re,
773 array(&$this, '_expandPlaceholdersCallback'),
774 $query
776 return $query;
781 * string _expandPlaceholdersCallback(list $m)
782 * Internal function to replace placeholders (see preg_replace_callback).
784 function _expandPlaceholdersCallback($m)
786 // Placeholder.
787 if (!empty($m[2])) {
788 $type = $m[3];
790 // Idenifier prefix.
791 if ($type == '_') {
792 return $this->_identPrefix;
795 // Value-based placeholder.
796 if (!$this->_placeholderArgs) return 'DBSIMPLE_ERROR_NO_VALUE';
797 $value = array_pop($this->_placeholderArgs);
799 // Skip this value?
800 if ($value === DBSIMPLE_SKIP) {
801 $this->_placeholderNoValueFound = true;
802 return '';
805 // First process guaranteed non-native placeholders.
806 switch ($type) {
807 case 'a':
808 if (!$value) $this->_placeholderNoValueFound = true;
809 if (!is_array($value)) return 'DBSIMPLE_ERROR_VALUE_NOT_ARRAY';
810 $parts = array();
811 foreach ($value as $k=>$v) {
812 $v = $v === null? 'NULL' : $this->escape($v);
813 if (!is_int($k)) {
814 $k = $this->escape($k, true);
815 $parts[] = "$k=$v";
816 } else {
817 $parts[] = $v;
820 return join(', ', $parts);
821 case "#":
822 // Identifier.
823 if (!is_array($value)) return $this->escape($value, true);
824 $parts = array();
825 foreach ($value as $table => $identifier) {
826 if (!is_string($identifier)) return 'DBSIMPLE_ERROR_ARRAY_VALUE_NOT_STRING';
827 $parts[] = (!is_int($table)? $this->escape($table, true) . '.' : '') . $this->escape($identifier, true);
829 return join(', ', $parts);
830 case 'n':
831 // NULL-based placeholder.
832 return empty($value)? 'NULL' : intval($value);
835 // Native arguments are not processed.
836 if ($this->_placeholderNativeArgs !== null) {
837 $this->_placeholderNativeArgs[] = $value;
838 return $this->_performGetNativePlaceholderMarker(count($this->_placeholderNativeArgs) - 1);
841 // In non-native mode arguments are quoted.
842 if ($value === null) return 'NULL';
843 switch ($type) {
844 case '':
845 if (!is_scalar($value)) return 'DBSIMPLE_ERROR_VALUE_NOT_SCALAR';
846 return $this->escape($value);
847 case 'd':
848 return intval($value);
849 case 'f':
850 return str_replace(',', '.', floatval($value));
852 // By default - escape as string.
853 return $this->escape($value);
856 // Optional block.
857 if (isset($m[1]) && strlen($block=$m[1])) {
858 $prev = @$this->_placeholderNoValueFound;
859 $block = $this->_expandPlaceholdersFlow($block);
860 $block = $this->_placeholderNoValueFound? '' : ' ' . $block . ' ';
861 $this->_placeholderNoValueFound = $prev; // recurrent-safe
862 return $block;
865 // Default: skipped part of the string.
866 return $m[0];
871 * void _setLastError($code, $msg, $query)
872 * Set last database error context.
873 * Aditionally expand placeholders.
875 function _setLastError($code, $msg, $query)
877 if (is_array($query)) {
878 $this->_expandPlaceholders($query, false);
879 $query = $query[0];
881 return DbSimple_Generic_LastError::_setLastError($code, $msg, $query);
886 * Return microtime as float value.
888 function _microtime()
890 $t = explode(" ", microtime());
891 return $t[0] + $t[1];
896 * Convert SQL field-list to COUNT(...) clause
897 * (e.g. 'DISTINCT a AS aa, b AS bb' -> 'COUNT(DISTINCT a, b)').
899 function _fieldList2Count($fields)
901 $m = null;
902 if (preg_match('/^\s* DISTINCT \s* (.*)/sx', $fields, $m)) {
903 $fields = $m[1];
904 $fields = preg_replace('/\s+ AS \s+ .*? (?=,|$)/sx', '', $fields);
905 return "COUNT(DISTINCT $fields)";
906 } else {
907 return 'COUNT(*)';
913 * array _transformResult(list $rows)
914 * Transform resulting rows to various formats.
916 function _transformResult($rows)
918 // Process ARRAY_KEY feature.
919 if (is_array($rows) && $rows) {
920 // Find ARRAY_KEY* AND PARENT_KEY fields in field list.
921 $pk = null;
922 $ak = array();
923 foreach (current($rows) as $fieldName => $dummy) {
924 if (0 == strncasecmp($fieldName, DBSIMPLE_ARRAY_KEY, strlen(DBSIMPLE_ARRAY_KEY))) {
925 $ak[] = $fieldName;
926 } else if (0 == strncasecmp($fieldName, DBSIMPLE_PARENT_KEY, strlen(DBSIMPLE_PARENT_KEY))) {
927 $pk = $fieldName;
930 natsort($ak); // sort ARRAY_KEY* using natural comparision
932 if ($ak) {
933 // Tree-based array? Fields: ARRAY_KEY, PARENT_KEY
934 if ($pk !== null) {
935 return $this->_transformResultToForest($rows, $ak[0], $pk);
937 // Key-based array? Fields: ARRAY_KEY.
938 return $this->_transformResultToHash($rows, $ak);
941 return $rows;
946 * Converts rowset to key-based array.
948 * @param array $rows Two-dimensional array of resulting rows.
949 * @param array $ak List of ARRAY_KEY* field names.
950 * @return array Transformed array.
952 function _transformResultToHash($rows, $arrayKeys)
954 $arrayKeys = (array)$arrayKeys;
955 $result = array();
956 foreach ($rows as $row) {
957 // Iterate over all of ARRAY_KEY* fields and build array dimensions.
958 $current =& $result;
959 foreach ($arrayKeys as $ak) {
960 $key = $row[$ak];
961 unset($row[$ak]); // remove ARRAY_KEY* field from result row
962 if ($key !== null) {
963 $current =& $current[$key];
964 } else {
965 // IF ARRAY_KEY field === null, use array auto-indices.
966 $tmp = array();
967 $current[] =& $tmp;
968 $current =& $tmp;
969 unset($tmp); // we use $tmp, because don't know the value of auto-index
972 $current = $row; // save the row in last dimension
974 return $result;
979 * Converts rowset to the forest.
981 * @param array $rows Two-dimensional array of resulting rows.
982 * @param string $idName Name of ID field.
983 * @param string $pidName Name of PARENT_ID field.
984 * @return array Transformed array (tree).
986 function _transformResultToForest($rows, $idName, $pidName)
988 $children = array(); // children of each ID
989 $ids = array();
990 // Collect who are children of whom.
991 foreach ($rows as $i=>$r) {
992 $row =& $rows[$i];
993 $id = $row[$idName];
994 if ($id === null) {
995 // Rows without an ID are totally invalid and makes the result tree to
996 // be empty (because PARENT_ID = null means "a root of the tree"). So
997 // skip them totally.
998 continue;
1000 $pid = $row[$pidName];
1001 if ($id == $pid) $pid = null;
1002 $children[$pid][$id] =& $row;
1003 if (!isset($children[$id])) $children[$id] = array();
1004 $row['childNodes'] =& $children[$id];
1005 $ids[$id] = true;
1007 // Root elements are elements with non-found PIDs.
1008 $forest = array();
1009 foreach ($rows as $i=>$r) {
1010 $row =& $rows[$i];
1011 $id = $row[$idName];
1012 $pid = $row[$pidName];
1013 if ($pid == $id) $pid = null;
1014 if (!isset($ids[$pid])) {
1015 $forest[$row[$idName]] =& $row;
1017 unset($row[$idName]);
1018 unset($row[$pidName]);
1020 return $forest;
1025 * Replaces the last array in a multi-dimensional array $V by its first value.
1026 * Used for selectCol(), when we need to transform (N+1)d resulting array
1027 * to Nd array (column).
1029 function _shrinkLastArrayDimensionCallback(&$v)
1031 if (!$v) return;
1032 reset($v);
1033 if (!is_array($firstCell = current($v))) {
1034 $v = $firstCell;
1035 } else {
1036 array_walk($v, array(&$this, '_shrinkLastArrayDimensionCallback'));
1042 * void _logQuery($query, $noTrace=false)
1043 * Must be called on each query.
1044 * If $noTrace is true, library caller is not solved (speed improvement).
1046 function _logQuery($query, $noTrace=false)
1048 if (!$this->_logger) return;
1049 $this->_expandPlaceholders($query, false);
1050 $args = array();
1051 $args[] =& $this;
1052 $args[] = $query[0];
1053 $args[] = $noTrace? null : $this->findLibraryCaller();
1054 return call_user_func_array($this->_logger, $args);
1059 * void _logQueryStat($queryTime, $fetchTime, $firstFetchTime, $rows, $cached)
1060 * Log information about performed query statistics.
1062 function _logQueryStat($queryTime, $fetchTime, $firstFetchTime, $rows, $cached)
1064 // Always increment counters.
1065 $this->_statistics['time'] += $queryTime;
1066 $this->_statistics['count']++;
1067 if ($cached) $this->_statistics['cache']++;
1069 // If no logger, economize CPU resources and actually log nothing.
1070 if (!$this->_logger) return;
1072 $dt = round($queryTime * 1000);
1073 $firstFetchTime = round($firstFetchTime*1000);
1074 $tailFetchTime = round($fetchTime * 1000) - $firstFetchTime;
1075 $log = " -- ";
1076 if ($firstFetchTime + $tailFetchTime) {
1077 $log = sprintf(($cached?'> <b>cached</b>':'')." -- %d ms = %d+%d".($tailFetchTime? "+%d" : ""), $dt, $dt-$firstFetchTime-$tailFetchTime, $firstFetchTime, $tailFetchTime);
1078 } else {
1079 $log = sprintf(($cached?'> <b>cached</b>':'')." -- %d ms", $dt);
1081 $log .= "; returned ";
1083 if (!is_array($rows)) {
1084 $log .= $this->escape($rows);
1085 } else {
1086 $detailed = null;
1087 if (count($rows) == 1) {
1088 $len = 0;
1089 $values = array();
1090 foreach ($rows[0] as $k=>$v) {
1091 $len += strlen($v);
1092 if ($len > $this->MAX_LOG_ROW_LEN) {
1093 break;
1095 $values[] = $v === null? 'NULL' : $this->escape($v);
1097 if ($len <= $this->MAX_LOG_ROW_LEN) {
1098 $detailed = "(" . preg_replace("/\r?\n/", "\\n", join(', ', $values)) . ")";
1101 if ($detailed) {
1102 $log .= $detailed;
1103 } else {
1104 $log .= count($rows). " row(s)";
1108 $this->_logQuery($log, true);
1112 * mixed _cache($hash, $result=null)
1113 * Calls cache mechanism if possible.
1115 function _cache($hash, $result=null)
1117 if (is_callable($this->_cacher)) {
1118 return call_user_func($this->_cacher, $hash, $result);
1119 } else if (is_object($this->_cacher) && method_exists($this->_cacher, 'get') && method_exists($this->_cacher, 'save')) {
1120 if (null === $result)
1121 return $this->_cacher->get($hash);
1122 else
1123 $this->_cacher->save($result, $hash);
1125 else return false;
1130 * protected constructor(string $dsn)
1132 * Prevent from direct creation of this object.
1134 function DbSimple_Generic_Database()
1136 die("This is protected constructor! Do not instantiate directly at ".__FILE__." line ".__LINE__);
1139 // Identifiers prefix (used for ?_ placeholder).
1140 var $_identPrefix = '';
1142 // Queries statistics.
1143 var $_statistics = array(
1144 'time' => 0,
1145 'count' => 0,
1146 'cache' =>0,
1149 var $_cachePrefix = '';
1151 var $_logger = null;
1152 var $_cacher = null;
1153 var $_placeholderArgs, $_placeholderNativeArgs, $_placeholderCache=array();
1154 var $_placeholderNoValueFound;
1157 * When string representation of row (in characters) is greater than this,
1158 * row data will not be logged.
1160 var $MAX_LOG_ROW_LEN = 128;
1165 * Database BLOB.
1166 * Can read blob chunk by chunk, write data to BLOB.
1168 class DbSimple_Generic_Blob extends DbSimple_Generic_LastError
1171 * string read(int $length)
1172 * Returns following $length bytes from the blob.
1174 function read($len)
1176 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
1180 * string write($data)
1181 * Appends data to blob.
1183 function write($data)
1185 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
1189 * int length()
1190 * Returns length of the blob.
1192 function length()
1194 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
1198 * blobid close()
1199 * Closes the blob. Return its ID. No other way to obtain this ID!
1201 function close()
1203 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
1209 * Support for error tracking.
1210 * Can hold error messages, error queries and build proper stacktraces.
1212 class DbSimple_Generic_LastError
1214 var $error = null;
1215 var $errmsg = null;
1216 var $errorHandler = null;
1217 var $ignoresInTraceRe = 'DbSimple_.*::.* | call_user_func.*';
1220 * abstract void _logQuery($query)
1221 * Must be overriden in derived class.
1223 function _logQuery($query)
1225 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);;
1229 * void _resetLastError()
1230 * Reset the last error. Must be called on correct queries.
1232 function _resetLastError()
1234 $this->error = $this->errmsg = null;
1238 * void _setLastError(int $code, string $message, string $query)
1239 * Fill $this->error property with error information. Error context
1240 * (code initiated the query outside DbSimple) is assigned automatically.
1242 function _setLastError($code, $msg, $query)
1244 $context = "unknown";
1245 if ($t = $this->findLibraryCaller()) {
1246 $context = (isset($t['file'])? $t['file'] : '?') . ' line ' . (isset($t['line'])? $t['line'] : '?');
1248 $this->error = array(
1249 'code' => $code,
1250 'message' => rtrim($msg),
1251 'query' => $query,
1252 'context' => $context,
1254 $this->errmsg = rtrim($msg) . ($context? " at $context" : "");
1256 $this->_logQuery(" -- error #".$code.": ".preg_replace('/(\r?\n)+/s', ' ', $this->errmsg));
1258 if (is_callable($this->errorHandler)) {
1259 call_user_func($this->errorHandler, $this->errmsg, $this->error);
1262 return null;
1267 * callback setErrorHandler(callback $handler)
1268 * Set new error handler called on database errors.
1269 * Handler gets 3 arguments:
1270 * - error message
1271 * - full error context information (last query etc.)
1273 function setErrorHandler($handler)
1275 $prev = $this->errorHandler;
1276 $this->errorHandler = $handler;
1277 // In case of setting first error handler for already existed
1278 // error - call the handler now (usual after connect()).
1279 if (!$prev && $this->error) {
1280 call_user_func($this->errorHandler, $this->errmsg, $this->error);
1282 return $prev;
1286 * void addIgnoreInTrace($reName)
1287 * Add regular expression matching ClassName::functionName or functionName.
1288 * Matched stack frames will be ignored in stack traces passed to query logger.
1290 function addIgnoreInTrace($name)
1292 $this->ignoresInTraceRe .= "|" . $name;
1296 * array of array findLibraryCaller()
1297 * Return part of stacktrace before calling first library method.
1298 * Used in debug purposes (query logging etc.).
1300 function findLibraryCaller()
1302 $caller = call_user_func(
1303 array(&$this, 'debug_backtrace_smart'),
1304 $this->ignoresInTraceRe,
1305 true
1307 return $caller;
1311 * array debug_backtrace_smart($ignoresRe=null, $returnCaller=false)
1313 * Return stacktrace. Correctly work with call_user_func*
1314 * (totally skip them correcting caller references).
1315 * If $returnCaller is true, return only first matched caller,
1316 * not all stacktrace.
1318 * @version 2.03
1320 function debug_backtrace_smart($ignoresRe=null, $returnCaller=false)
1322 if (!is_callable($tracer='debug_backtrace')) return array();
1323 $trace = $tracer();
1325 if ($ignoresRe !== null) $ignoresRe = "/^(?>{$ignoresRe})$/six";
1326 $smart = array();
1327 $framesSeen = 0;
1328 for ($i=0, $n=count($trace); $i<$n; $i++) {
1329 $t = $trace[$i];
1330 if (!$t) continue;
1332 // Next frame.
1333 $next = isset($trace[$i+1])? $trace[$i+1] : null;
1335 // Dummy frame before call_user_func* frames.
1336 if (!isset($t['file'])) {
1337 $t['over_function'] = $trace[$i+1]['function'];
1338 $t = $t + $trace[$i+1];
1339 $trace[$i+1] = null; // skip call_user_func on next iteration
1342 // Skip myself frame.
1343 if (++$framesSeen < 2) continue;
1345 // 'class' and 'function' field of next frame define where
1346 // this frame function situated. Skip frames for functions
1347 // situated in ignored places.
1348 if ($ignoresRe && $next) {
1349 // Name of function "inside which" frame was generated.
1350 $frameCaller = (isset($next['class'])? $next['class'].'::' : '') . (isset($next['function'])? $next['function'] : '');
1351 if (preg_match($ignoresRe, $frameCaller)) continue;
1354 // On each iteration we consider ability to add PREVIOUS frame
1355 // to $smart stack.
1356 if ($returnCaller) return $t;
1357 $smart[] = $t;
1359 return $smart;