false, // use milliseconds? 'precision' => 4, 'precisionMS' => 1, 'useHrtime' => null, 'suffix' => '', 'suffixMS' => 'ms', ); /** * Measure time between two events * * First call should be to $key = Debug::timer() with no params, or provide your own key that's not already been used * Second call should pass the key given by the first call to get the time elapsed, i.e. $time = Debug::timer($key). * Note that you may make multiple calls back to Debug::timer() with the same key and it will continue returning the * elapsed time since the original call. If you want to reset or remove the timer, call removeTimer or resetTimer. * * @param string $key * Leave blank to start timer. * Specify existing key (string) to return timer. * Specify new made up key to start a named timer. * @param bool $reset If the timer already exists, it will be reset when this is true. * @return string|int * */ static public function timer($key = '', $reset = false) { // returns number of seconds elapsed since first call if($reset && $key) self::removeTimer($key); if(!$key || !isset(self::$timers[$key])) { $value = self::startTimer($key); } else { $value = self::stopTimer($key, null, false); } return $value; } /** * Start a new timer * * @param string $key Optionally specify name for new timer * @return string * */ static public function startTimer($key = '') { if(self::$timerSettings['useHrtime'] === null) { self::$timerSettings['useHrtime'] = function_exists("\\hrtime"); } $startTime = self::$timerSettings['useHrtime'] ? hrtime(true) : -microtime(true); if($key === '') { $key = (string) $startTime; while(isset(self::$timers[$key])) $key .= "0"; } $key = (string) $key; self::$timers[$key] = $startTime; self::$lastTimerKey = $key; return $key; } /** * Get elapsed time for given timer and stop * * @param string $key Timer key returned by startTimer(), or omit for last started timer * @param null|int|string $option Specify override precision (int), suffix (string), or "ms" for milliseconds and suffix. * @param bool $clear Also clear the timer? (default=true) * @return string * @since 3.0.158 * */ static public function stopTimer($key = '', $option = null, $clear = true) { if(empty($key) && self::$lastTimerKey) $key = self::$lastTimerKey; if(!isset(self::$timers[$key])) return ''; $value = self::$timers[$key]; $useMS = $option === 'ms' || (self::$timerSettings['useMS'] && $option !== 's'); $suffix = $useMS ? self::$timerSettings['suffixMS'] : self::$timerSettings['suffix']; $precision = $useMS ? self::$timerSettings['precisionMS'] : self::$timerSettings['precision']; if(self::$timerSettings['useHrtime']) { // existing hrtime timer $value = ((hrtime(true) - $value) / 1e+6) / 1000; } else { // existing microtime timer $value = $value + microtime(true); } if($option === null) { // no option specified } else if(is_int($option)) { // precision override $precision = $option; } else if(is_string($option)) { // suffix specified $suffix = $option; } if($useMS) { $value = round($value * 1000, $precision); } else { $value = number_format($value, $precision); } if($clear) self::removeTimer($key); if($suffix) $value .= $suffix; return $value; } /** * Get or set timer setting * * ~~~~~~ * // Example of changing precision to 2 * Debug::timerSetting('precision', 2); * ~~~~~~ * * @param string $key * @param mixed|null $value * @return mixed * @since 3.0.154 * */ static public function timerSetting($key, $value = null) { if($value !== null) self::$timerSettings[$key] = $value; return self::$timerSettings[$key]; } /** * Save the current time of the given timer which can be later retrieved with getSavedTimer($key) * * Note this also stops/removes the timer. * * @param string $key * @param string $note Optional note to include in getSavedTimer * @return bool|string Returns elapsed time, or false if timer didn't exist * */ static public function saveTimer($key, $note = '') { if(!isset(self::$timers[$key])) return false; self::$savedTimers[$key] = self::stopTimer($key); if($note) self::$savedTimerNotes[$key] = $note; return self::$savedTimers[$key]; } /** * Return the time recorded in the saved timer $key * * @param string $key * @return string Blank if timer not recognized * */ static public function getSavedTimer($key) { $value = isset(self::$savedTimers[$key]) ? self::$savedTimers[$key] : null; if(!is_null($value) && isset(self::$savedTimerNotes[$key])) $value = "$value - " . self::$savedTimerNotes[$key]; return (string) $value; } /** * Return all saved timers in associative array indexed by key * * @return array * */ static public function getSavedTimers() { $timers = self::$savedTimers; arsort($timers); foreach($timers as $key => $timer) { $timers[$key] = self::getSavedTimer($key); // to include notes } return $timers; } /** * Remove a previously saved timer * * @param string $key * @since 3.0.202 * */ static public function removeSavedTimer($key) { unset(self::$savedTimers[$key]); unset(self::$savedTimerNotes[$key]); } /** * Remove all saved timers * * @since 3.0.202 * */ static public function removeSavedTimers() { self::$savedTimers = array(); self::$savedTimerNotes = array(); } /** * Reset a timer so that it starts timing again from right now * * @param string $key * @return string|int * */ static public function resetTimer($key) { self::removeTimer($key); return self::timer($key); } /** * Remove a timer completely * * @param string $key * */ static public function removeTimer($key) { unset(self::$timers[$key]); } /** * Remove all active timers * */ static public function removeAll() { self::$timers = array(); } /** * Get all active timers in array with timer name (key) and start time (value) * * @return array * @since 3.0.158 * */ static public function getAll() { return self::$timers; } /** * Return a backtrace array that is simpler and more PW-specific relative to PHP’s debug_backtrace * * @param array $options * - `limit` (int): The limit for the backtrace or 0 for no limit. (default=0) * - `flags` (int): Flags as used by PHP’s debug_backtrace() function. (default=DEBUG_BACKTRACE_PROVIDE_OBJECT) * - `showHooks` (bool): Show inernal methods for hook calls? (default=false) * - `getString` (bool): Get newline separated string rather than array? (default=false) * - `getCnt` (bool): Get index number count, used for getString option only. (default=true) * - `getFile` (bool|string): Get filename? Specify one of true, false or 'basename'. (default=true) * - `maxCount` (int): Max size for arrays (default=10) * - `maxStrlen` (int): Max length for strings (default=100) * - `maxDepth` (int): Max allowed recursion depth when converting variables to strings. (default=5) * - `ellipsis` (string): Show this ellipsis when a long value is truncated (default='…') * - `skipCalls` (array): Method/function calls to skip. * @return array|string * @since 3.0.136 * */ static public function backtrace(array $options = array()) { $defaults = array( 'limit' => 0, // the limit argument for the debug_backtrace call 'flags' => DEBUG_BACKTRACE_PROVIDE_OBJECT, // flags for PHP debug_backtrace function 'showHooks' => false, // show internal methods for hook calls? 'getString' => false, // get newline separated string rather than array? 'getCnt' => true, // get index number count (for getString only) 'getFile' => true, // get filename? true, false or 'basename' 'maxCount' => 10, // max size for arrays 'maxStrlen' => 100, // max length for strings 'maxDepth' => 5, // max allowed recursion depth when converting variables to strings 'ellipsis' => ' …', // show this ellipsis when a long value is truncated 'skipCalls' => array(), // method/function calls to skip ); $options = array_merge($defaults, $options); if($options['limit']) $options['limit']++; $traces = @debug_backtrace($options['flags'], $options['limit']); $config = wire()->config; $rootPath = ProcessWire::getRootPath(true); $rootPath2 = $config && $config->paths ? $config->paths->root : $rootPath; array_shift($traces); // shift of the simpleBacktrace call, which is not needed $apiVars = array(); $result = array(); $cnt = 0; foreach(wire('all') as $name => $value) { if(!is_object($value)) continue; $apiVars[wireClassName($value)] = '$' . $name; } foreach($traces as $trace) { if(!is_array($trace) || !isset($trace['function']) || !isset($trace['file'])) { continue; } else if(count($options['skipCalls']) && in_array($trace['function'], $options['skipCalls'])) { continue; } $obj = null; $class = ''; $type = ''; $args = $trace['args']; $argStr = ''; $file = $trace['file']; $basename = basename($file); $function = $trace['function']; $isHookableCall = false; if(isset($trace['object'])) { $obj = $trace['object']; $class = wireClassName($obj); } else if(isset($trace['class'])) { $class = wireClassName($trace['class']); } if($class) { $type = isset($trace['type']) ? $trace['type'] : '.'; } if(!$options['showHooks']) { if($basename === 'Wire.php' && !wireMethodExists('Wire', $function)) continue; if($class === 'WireHooks' || $basename === 'WireHooks.php') continue; } if(strpos($function, '___') === 0) { $isHookableCall = '___'; } else if($obj && !method_exists($obj, $function) && method_exists($obj, "___$function")) { $isHookableCall = true; } if($type === '->' && isset($apiVars[$class])) { // use API var name when available if(strtolower($class) === strtolower(ltrim($apiVars[$class], '$'))) { $class = $apiVars[$class]; } else { $class = "$class " . $apiVars[$class]; } } if($basename === 'Wire.php' && $class !== 'Wire') { $ref = new \ReflectionClass($trace['class']); $file = $ref->getFileName(); } // rootPath and rootPath2 can be different if one of them represented by a symlink $file = str_replace($rootPath, '/', $file); if($rootPath2 !== $rootPath) $file = str_replace($rootPath2, '/', $file); if(($function === '__call' || $function == '_callMethod') && count($args)) { $function = array_shift($args); } if(!$options['showHooks'] && $isHookableCall === '___') { $function = substr($function, 3); } if(!empty($args)) { $newArgs = array(); if($isHookableCall && count($args) === 1 && is_array($args[0])) { $newArgs = $args[0]; } foreach($args as $arg) { if(is_object($arg)) { $arg = wireClassName($arg) . ' $obj'; } else if(is_array($arg)) { $count = count($arg); if($count < 4) { $arg = $count ? self::traceStr($arg, array('maxDepth' => 2)) : '[]'; } else { $arg = 'array(' . count($arg) . ')'; } } else if(is_string($arg)) { if(strlen("$arg") > $options['maxStrlen']) $arg = substr($arg, 0, $options['maxStrlen']) . ' …'; $arg = '"' . $arg . '"'; } else if(is_bool($arg)) { $arg = $arg ? 'true' : 'false'; } else if($arg === null) { $arg = 'null'; } else { // leave as-is (int, float, etc.) } $newArgs[] = $arg; } $argStr = implode(', ', $newArgs); if($argStr === '[]') $argStr = ''; } if($options['getFile'] === 'basename') $file = basename($file); $call = "$class$type$function($argStr)"; $file = "$file:$trace[line]"; if($options['getString']) { $str = ''; if($options['getCnt']) $str .= "$cnt. "; $str .= "$file » $call"; $result[] = $str; } else { $result[] = array( 'file' => $file, 'call' => $call, ); } $cnt++; } if($options['getString']) $result = implode("\n", $result); return $result; } /** * Convert value to string for backtrace method * * @param $value * @param array $options * @return null|string * */ static protected function traceStr($value, array $options = array()) { $defaults = array( 'maxCount' => 10, // max size for arrays 'maxStrlen' => 100, // max length for strings 'maxDepth' => 5, 'ellipsis' => ' …' ); static $depth = 0; $options = count($options) ? array_merge($defaults, $options) : $defaults; $depth++; if(is_object($value)) { // object $str = wireClassName($value); if($str === 'HookEvent') { $str .= ' $event'; } else if(method_exists($value, '__toString')) { $value = (string) $value; if($value !== $str) { if(strlen($value) > $options['maxStrlen']) { $value = substr($value, 0, $options['maxStrlen']) . $options['ellipsis']; } $str .= "($value)"; } } } else if(is_array($value)) { // array if(empty($value)) { $str = '[]'; } else if($depth >= $options['maxDepth']) { $str = "array(" . count($value) . ")"; } else { $suffix = ''; if(count($value) > $options['maxCount']) { $value = array_slice($value, 0, $options['maxCount']); $suffix = $options['ellipsis']; } foreach($value as $k => $v) { if(is_string($k) && strlen($k)) { $value[$k] = "$$k => " . self::traceStr($v, $options); } else { $value[$k] = self::traceStr($v, $options); } } $str = '[ ' . implode(', ', $value) . $suffix . ' ]'; } } else if(is_string($value)) { // string if(strlen($value) > $options['maxStrlen']) { $value = substr($value, 0, $options['maxStrlen']) . $options['ellipsis']; } $hasDQ = strpos($value, '"') !== false; $hasSQ = strpos($value, "'") !== false; if(($hasDQ && $hasSQ) || $hasSQ) { $value = str_replace('"', '\\"', $value); $str = '"' . $value . '"'; } else { $str = "'$value'"; } } else if(is_bool($value)) { // true or false $str = $value ? 'true' : 'false'; } else { // int, float or other $str = $value; } $depth--; return $str; } /** * Dump any variable to a debug string * * @param int|float|object|string|array $value * @param array $options * - `method` (string): Dump method to use, one of: json_encode, var_dump, var_export, print_r (default=json_encode) * - `html` (bool): Return output-ready HTML string? (default=false) * @return string * @since 3.0.208 * */ static public function toStr($value, array $options = array()) { $defaults = array( 'method' => 'json_encode', 'html' => false, ); $options = array_merge($defaults, $options); $method = $options['method']; $prepend = ''; if(is_object($value)) { // we format objects to arrays or strings $className = wireClassName($value); $classInfo = "object:$className"; $objectValue = $value; if($objectValue instanceof \Countable) { $classInfo .= '(' . count($objectValue) . ')'; } if($value instanceof Wire) { $value = $value->debugInfoSmall(); } else if(method_exists($value, '__debugInfo')) { $value = $value->__debugInfo(); } else if(method_exists($value, '__toString')) { $value = $classInfo . ":\"$value\""; } else { $value = $classInfo; } if(is_array($value)) { if(empty($value)) { $value = $classInfo; if(method_exists($objectValue, '__toString')) { $stringValue = (string) $objectValue; if($stringValue != $className) $value .= ":\"$stringValue\""; } } else { $prepend = "$classInfo "; } } if(is_string($value)) { $method = ''; } } else if(is_int($value)) { $prepend = 'int:'; } else if(is_float($value)) { $prepend = 'float:'; } else if(is_string($value)) { $prepend = 'string:'; } else if(is_callable($value)) { $prepend = 'callable:'; } else if(is_resource($value)) { $prepend = 'resource:'; } switch($method) { case 'json_encode': $value = json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $value = str_replace(' ', ' ', $value); if(strpos($value, '\\"') !== false) $value = str_replace('\\"', "'", $value); break; case 'var_export': $value = var_export($value, true); break; case 'var_dump': ob_start(); var_dump($value); $value = ob_get_contents(); ob_end_clean(); break; case 'print_r': $value = print_r($value, true); break; default: $value = (string) $value; } if($method && $method != 'json_encode') { // array is obvious and needs no label if(stripos($value, 'array') === 0) $value = trim(substr($value, 5)); } if($prepend) $value = $prepend . trim($value); if($options['html']) $value = '
' . wire()->sanitizer->entities($value) . '
'; return $value; } }