Current File : //opt/RZphp74/includes/PHP/FunctionCallTracer.php |
<?php
/**
* Function Call Tracer
*
* Creates a function calls debug trace. Functions arguments, returned parameters
* and watched variables are reported in the same section for each function call.
* The trace is available as an array, or can be displayed or written in a file.
* Traced variables can be processed by provided user functions for displaying
* purposes.
*
* This package is not a replacement for full fledged PHP debuggers. It is
* useful for (1) remote debugging, (2) to debug a complex sequence of function
* calls, (3) to display non text variables in a user readable format.
*
* (1) Remote debugging is sometimes the only option to debug a package that
* works fine on your system, e.g. a 32-bit OS, but breaks on a different system,
* e.g. a 64-bit OS, which you have no access to. A remote user who has the
* latter OS could run the package, then send you the trace for analysis.
*
* (2) It is sometimes difficult not to loose track of functions calls in some
* live debugging sessions even with top notch PHP editor/debuggers. The trace
* produced by this package may come handy and is easy to use in combination
* with the source code to track calls and variables.
*
* (3) Some variables native format does not always display well, typically:
* packed data and UTF-8 strings. They can be converted as they are being traced
* to a readable format by provided user functions. For example: converting
* binary strings to hexadecimal, or UTF-8 string to Unicode.
*
* Fully tested with phpUnit. Code coverage test close to 100%.
*
* Usage including trace examples is fully documented in docs/examples files.
*
* PHP version 5
*
* All rights reserved.
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
* + Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* + Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation and/or
* other materials provided with the distribution.
* + The name of its contributors may not be used to endorse or promote products
* derived from this software without specific prior written permission.
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @category PHP
* @package PHP_FunctionCallTracer
* @author Michel Corne <mcorne@yahoo.com>
* @copyright 2007 Michel Corne
* @license http://www.opensource.org/licenses/bsd-license.php The BSD License
* @version SVN: $Id: FunctionCallTracer.php 18 2007-08-04 09:05:18Z mcorne $
* @link http://pear.php.net/package/PHP_FunctionCallTracer
*/
/**
* Function Call Tracer
*
* Traces functions arguments, returned parameters, and watched variables.
* Sets user functions and processes any of the above.
*
* Main methods:
* + self::traceArguments() traces the function arguments.
* + self::traceReturn() traces the returned parameters by the function.
* + self::traceVariables() traces variables within the function.
* + self::setUserFunctions() sets user functions to process variables.
* + self::processVariables() processes variables with user functions.
* + self::getTrace() returns the function calls trace.
* + self::putTrace() displays or writes the function calls trace in a file.
*
* Usage including trace examples is fully documented in docs/examples files.
*
* Some basic examples:
* <pre>
* Example 1: tracing argument: $a, variable: $b, returned parameter: $c
*
* require_once 'PHP/FunctionCallTracer.php';
*
* function foo($a)
* {
* PHP_FunctionCallTracer::traceArguments();
*
* $b = strtoupper($a);
* PHP_FunctionCallTracer::traceVariables($b);
*
* $c = true;
* PHP_FunctionCallTracer::traceReturn($c);
* return $c;
* }
*
* $c = foo('foo');
* PHP_FunctionCallTracer::putTrace();
*
* Example 2: tracing and rounding variable: $a
*
* require_once 'PHP/FunctionCallTracer.php';
*
* PHP_FunctionCallTracer::setUserFunctions('round');
*
* function bar($a)
* {
* $a *= 2;
* PHP_FunctionCallTracer::traceVariables($a);
* PHP_FunctionCallTracer::processVariables();
*
* return $a;
* }
*
* $a = bar(1.23);
* PHP_FunctionCallTracer::putTrace();
* </pre>
*
* @category PHP
* @package PHP_FunctionCallTracer
* @author Michel Corne <mcorne@yahoo.com>
* @copyright 2007 Michel Corne
* @license http://www.opensource.org/licenses/bsd-license.php The BSD License
* @version Release:@package_version@
* @link http://pear.php.net/package/PHP_FunctionCallTracer
*/
class PHP_FunctionCallTracer
{
/**
* The list of function calls
*
* @var array
* @access private
* @static
*/
private static $calls = array();
/**
* The function call ID stack
*
* @var array
* @access private
* @static
*/
private static $callIdStack = array();
/**
* The list of invalid user functions
*
* @var array
* @access private
* @static
*/
private static $invalidFct = array();
/**
* The multi-user function flag
*
* True if 2 or more user-functions, false otherwise
*
* @var boolean
* @access private
* @static
*/
private static $isMultiUserFct = false;
/**
* The list objects calling the methods being traced
*
* @var array
* @access private
* @static
*/
private static $objects = array();
/**
* The list of callable user functions
*
* @var array
* @access private
* @static
*/
private static $userFct = array();
/**
* Filters some key/values of a function calls trace
*
* @param array $trace the function call trace
* @param array $keys the keys to keep
* @return array the filtered function call trace
* @access public
* @static
*/
public static function filterTrace($trace, $keys)
{
is_array($trace) or $trace = array($trace);
is_array($keys) or $keys = array($keys);
$filtered = array();
foreach($keys as $key) {
isset($trace[$key]) and $filtered[$key] = $trace[$key];
}
return $filtered;
}
/**
* Creates a function call ID stack key
*
* Based on the backtrace. Ignores the last function call.
* Retains only the data clearly identifying the call:
* 'file', 'line', 'class', 'type', 'function'. Serializes the backtrace to
* build the key.
*
* @param array $trace the function call backtrace
* @return array the function call ID stack key
* @access public
* @static
*/
public static function createStackKey($trace)
{
array_shift($trace);
foreach($trace as &$value) {
$value = self::filterTrace($value,
array('file', 'line', 'class', 'type', 'function'));
}
return serialize($trace);
}
/**
* Gets the function calls trace
*
* This method is usually called at the end of a debugging session.
*
* @param boolean $getPhpInfo the phpinfo is to be captured if true,
* or not if false, the default is true
* @return array the function calls trace
* @access public
* @static
*/
public static function getTrace($getPhpInfo = true)
{
// captures the PHP version details and the current date
$trace['php_uname'] = php_uname();
date_default_timezone_set('UTC');
$trace['date'] = date(DATE_COOKIE);
// captures the user functions and the unavailable ones
self::$userFct and $trace['user_functions'] =
array_map(array(__CLASS__, 'tidyMethodName'), self::$userFct);
self::$invalidFct and $trace['invalid_user_functions'] =
array_map(array(__CLASS__, 'tidyMethodName'), self::$invalidFct);
// captures the function/methods calls and the objects details
$trace['calls'] = self::$calls;
self::$objects and $trace['objects'] = self::$objects;
// captures the PHP info
$getPhpInfo and ob_start() and phpinfo() and
$trace['phpinfo'] = ob_get_contents() and ob_end_clean();
return $trace;
}
/**
* Determines if the provided user function is callable
*
* @param array $function the user function, e.g. 'dechex',
* or object method, e.g. array($object, 'foo'),
* or static method, e.g. array('foo', 'bar')
* @return boolean true if callable, false if not
* @access public
* @static
*/
public static function isCallable($function)
{
$isCallable = false;
if (is_callable($function, true)) {
// a syntax compliant function or object/class method
if (is_string($function) or is_object($function[0])) {
// checks if a callable function, e.g. 'dechex',
// or a callable object method, e.g array($object, 'foo')
$isCallable = is_callable($function);
} else if (class_exists($function[0])) {
// checks if the class method exists, e.g. array('Foo', 'bar')
$isCallable = in_array($function[1], get_class_methods($function[0]));
}
}
return $isCallable;
}
/**
* Processes a set of variables with provided user functions
*
* It is usually called after self::traceArguments(), self::traceReturn(),
* or self::traceVariables(). It will process the variables passed to
* the above functions.
* This method is used in 2 different ways.
*
* (1) If the package is used with one user function,
* e.g. self::setUserFunctions('dechex'), this method is expecting
* as arguments the keys of variables that were passed to the
* above functions. Examples when called after
* self::traceArguments($a, $b, $c):
* + self::processVariables() => $a, $b, $c are processed.
* + self::processVariables(true) => $a, $b, $c are processed.
* + self::processVariables(0) => $a only is processed.
* + self::processVariables(0,1) => $a and $b only are processed.
*
* (2) If the package is used with 2 or more user function :
* e.g. self::setUserFunctions('dechex', 'bin2hex'), this method is expecting
* for each argument a set of keys of variables that were passed to the
* above functions. Examples when called after
* self::traceArguments($a, $b, $c):
* + self::processVariables() => $a, $b, $c are processed by dechex.
* + self::processVariables(null, true) => $a, $b, $c are processed by bin2hex.
* + self::processVariables(0, 1) => $a is processed by dechex and $b by bin2hex.
* + self::processVariables(0, array(1, 2)) => $a is processed by dexhex, and $b and $c by bin2hex.
*
* Automatically calls self::traceArguments() if this method is the first one
* called within a function.
*
* @return boolean false if this method is not called by a function,
* true otherwise (actually the function call trace detail)
* @access public
* @static
*/
public static function processVariables()
{
$trace = debug_backtrace();
if (!isset($trace[1])) {
// this method is expected to be called by a function
return false;
}
// gets the function call ID
// creates a call entry for the function if absent
$key = self::createStackKey($trace);
isset(self::$callIdStack[$key]) or self::traceArguments();
$id = self::$callIdStack[$key];
// extracts the function call last entered arguments, variables or
// returned parameters
$entry = end(self::$calls[$id]);
if (!isset($entry['args'])) {
// extracts the last traced variable
end($entry);
list($lastVarKey, $entry) = each($entry);
}
$variables = $entry['args'];
// captures the list of variables keys to process
$toProcess = func_get_args();
// sets all variables to be processed if empty
$toProcess === array() and $toProcess = array(true);
self::$isMultiUserFct or $toProcess === array(true) or
$toProcess = array($toProcess);
// sets all variables to be processed if true for the corresponding
// user function, e.g. array(1 => true),
// ignores the other function variable keys
$key = array_search(true, $toProcess, true);
$key === false or $toProcess = array($key => array_keys($variables));
$processed = array();
foreach($toProcess as $fctID => $varKeys) {
is_array($varKeys) or $varKeys = array($varKeys);
foreach($varKeys as $key) {
// processes the variable with the user function
is_array($key) or isset($variables[$key]) and
isset(self::$userFct[$fctID]) and $processed[$key] =
call_user_func(self::$userFct[$fctID], $variables[$key]);
}
}
// resorts and adds the processed variables to the function calls trace
ksort($processed);
$varType = key(self::$calls[$id]);
if (isset($lastVarKey)) {
self::$calls[$id][$varType][$lastVarKey]['args_p'] = $processed;
} else {
self::$calls[$id][$varType]['args_p'] = $processed;
}
return self::$calls[$id];
}
/**
* Displays or writes the function calls trace in a file
*
* This method is usually called at the end of a debugging session.
*
* @param string $file the name of the file, or the standard ouput
* if empty or by default
* @param boolean $getPhpInfo the phpinfo is to be captured if true,
* or not if false, the default is true for a file
* and false for the standard output
* @return array the function calls trace
* @access public
* @static
*/
public static function putTrace($file = '', $getPhpInfo = null)
{
if ($file) {
// gets the trace including the phpinfo by default
// stores the trace in a file
is_null($getPhpInfo) and $getPhpInfo = true;
$trace = self::getTrace($getPhpInfo);
// stores the trace in a file
$content = print_r($trace, true);
@file_put_contents($file, $content) or
exit("Error! Cannot write the trace in the file: $file");
} else {
// gets the trace without the phpinfo by default
// displays the trace to the standard output
is_null($getPhpInfo) and $getPhpInfo = false;
$trace = self::getTrace($getPhpInfo);
print_r($trace);
}
return $trace;
}
/**
* Resets the class settings and working variables
*
* This method is needed when 2 or more debug sessions are run in a row.
* There is no need to call this method before self::setUserFunctions()
* which calls this method internally.
*
* @return void
* @access public
* @static
*/
public static function reset()
{
self::$calls = array();
self::$callIdStack = array();
self::$invalidFct = array();
self::$isMultiUserFct = false;
self::$objects = array();
self::$userFct = array();
}
/**
* Sets provided user functions
*
* Each argument is expected to be a callable function or
* object/class method. Examples:
* + self::setUserFunctions('dechex') => the PHP function dexhex
* + self::setUserFunctions(array($object, 'foo')) => the object method foo
* + self::setUserFunctions(array('foo', 'bar')) => the class method foo::bar
* + self::setUserFunctions('dechex', array('foo', 'bar')) => a combination of the PHP function dexhex and the class method foo::bar.
*
* The first argument is the user function/method #0, the second is #1 etc...
* This order number is used when passing variables keys to
* self::processVariables().
*
* Verifies that the user functions/methods are callable.
* Reports which ones are callable or not in the function calls trace.
*
* @return array the 2 sets of callable and invalid functions/methods
* @access public
* @static
*/
public static function setUserFunctions()
{
self::reset();
foreach(func_get_args() as $fctID => $function) {
if (self::isCallable($function)) {
// captures the valid function/method
self::$userFct[$fctID] = $function;
} else {
// captures the invalid function/method
self::$invalidFct[$fctID] = $function;
}
}
self::$isMultiUserFct = (count(self::$userFct) +
count(self::$invalidFct)) > 1;
return array(self::$userFct, self::$invalidFct);
}
/**
* Tidies the object/class method name
*
* @param mixed $function a function name, e.g. 'dechex', or an object
* method, e.g. array('class' => $object,
* 'type' => '->', 'function' => 'foo'), or a class
* method, e.g. array('class' => 'foo',
* 'type' => '::', 'function' => 'bar')
* @return string the tidied function/method name, e.g. 'dexhex', or
* 'blah=>foo', or 'foo::bar'
* @access public
* @static
*/
public static function tidyMethodName($function)
{
if (is_array($function)) {
// a class/object method, e.g. array('Foo', 'bar')
// extracts the class and method names
$class = current($function) or $class = '???';
$method = next($function) and is_string($method) or $method = '???';
if (is_object($class)) {
// an object method call, gets the class name
$class = get_class($class);
$separator = '->';
} else {
// a class static method call
is_string($class) or $class = '???';
$separator = '::';
}
// implodes the class and method names
$function = "$class$separator$method";
} else {
// else: a PHP function
$function and is_string($function) or $function = '???';
}
return $function;
}
/**
* Traces the argments passed to a function
*
* Usually called within a function before the rest of the function code.
* Normally, not expecting any arguments since they are captured automaticly.
* However, if the function expects arguments passed by reference, the
* arguments should be passed explicitly, due to PHP Bug 42058:
* "debug_backtrace messes up with references". For example, given the
* function foo($a, &$b): self::traceArguments($a, $b) should be used
* instead of self::traceArguments() to trace arguments properly.
*
* Called automaticly by self::traceReturn(), self::traceVariables() or
* self::processVariables() if it is not called explicitly before any of
* those methods within the function.
*
* It should always be called if the function itself calls other functions
* that are being traced as well, in order to keep the function calls in
* order within the trace.
*
* @return boolean false if this method is not called by a function,
* true otherwise (actually the function call trace detail)
* @access public
* @static
*/
public static function traceArguments()
{
$trace = debug_backtrace();
if (!isset($trace[1])) {
// this method is expected to be called by a function
return false;
}
// skips one level if called internally by traceReturn()
isset($trace[1]['class']) and $trace[1]['class'] == __CLASS__ and
array_shift($trace);
// skips 2 levels if called internally via traceVariables()
$tracingReturn = isset($trace[0]['file']) or
array_shift($trace) and array_shift($trace);
// extracts the function call position
$call = self::filterTrace($trace[1], array('file', 'line'));
// reformats the function name
$function = self::filterTrace($trace[1], array('class', 'type', 'function'));
$call['function'] = implode('', $function);
// extracts the function entry point position and arguments
// cannot trust $trace[0]['args']: use func_get_args() if arguments passed
$in = self::filterTrace($trace[0], array('file', 'line'));
$in['args'] = ($args = func_get_args())? $args : $trace[1]['args'];
// captures the function call details
$id = count(self::$calls);
self::$calls[] = array('call' => $call, 'in' => $in);
if (isset($trace[1]['object'])) {
// captures or makes a reference to the (method) object details
$ref = array_search($trace[1]['object'], self::$objects, true);
self::$objects[$id] = $ref === false? $trace[1]['object'] : "same as #$ref";
}
// adds the function call to the call stack
$key = self::createStackKey($trace);
self::$callIdStack[$key] = $id;
return self::$calls[$id];
}
/**
* Traces the return parameters
*
* Optionally called within a function before a return statement.
* Normally expecting the parameters returned by the function, plus
* optionally any other variables. For example, given the return($a, $b)
* statement in a function: self::traceReturn($a, $b) should
* preceed this return statement.
*
* Automatically calls self::traceArguments() if it is not called explicitly
* before this method within the function.
*
* @return boolean false if this method is not called by a function,
* true otherwise (actually the function call trace detail)
* @access public
* @static
*/
public static function traceReturn()
{
$trace = debug_backtrace();
// skips 2 levels if called internally by traceVariables()
$tracingReturn = isset($trace[0]['file']) or array_shift($trace) and array_shift($trace);
if (!isset($trace[1])) {
// this method is expected to be called by a function
return false;
}
// extracts the function return position and parameters
$out = self::filterTrace($trace[0], array('file', 'line'));
$out['args'] = func_get_args();
// gets the function call ID, creates a call entry for the function if absent
$key = self::createStackKey($trace);
isset(self::$callIdStack[$key]) or self::traceArguments();
$id = self::$callIdStack[$key];
// adds the return parameters or watched variables to the function trace
if ($tracingReturn) {
self::$calls[$id]['out'] = $out;
} else {
self::$calls[$id]['watches'][] = $out;
}
return self::$calls[$id];
}
/**
* Traces variables used by a function
*
* Expecting a list of variables. For example, self::traceVariables($a, $b).
*
* Automatically calls self::traceArguments() if it is not called explicitly
* before this method within the function.
*
* @return boolean false if this method is not called by a function,
* true otherwise (actually the function call trace detail)
* @access public
* @static
*/
public static function traceVariables()
{
$args = func_get_args();
return call_user_func_array(array(__CLASS__, 'traceReturn'), $args);
}
}
?>