Wednesday, January 9, 2013

PHP's debug_backtrace: a compact guide

Every time I need to use this function, I can't remember how it works.
  1. The array includes all call sites leading up to the current stack frame, but not actually the current one.  (Everything in the current frame is still in scope to you, so you can use __FILE__ and __LINE__ or your current variables directly.)
  2. The array is indexed with 0=innermost to N=outermost frame.
  3. Each array index gives you information related to the call site of the next frame inward / earlier in the array.  That is, $bt[0] gives you the immediate caller of your current point of execution.  $bt[0]['function'] refers to the function or method invocation that called you, e.g. if the main code executes foo(1), then inside function foo, $bt[0]['function'] is foo.  The file and line point to the file/line containing the call.
  4. When a 'class' key is present, it is the class of the line of code actually executing the call, i.e. what __CLASS__ is at the 'file' and 'line'.
  5. When an 'object' key is present, it has the actual object being used for dispatch; i.e. get_class($bt[$i]['object']) may return either the same value as 'class', or any descendant of that class.
  6. The 'type' key, when present, is either -> or :: for dynamic or static calls, respectively.  The latter means that the 'object' key won't be set.
  7. There is no way in my PHP (5.3.3-14_el6.3 from CentOS updates) to view the invoked class of a static call, e.g. if SubThing::foo is called but Thing::foo is executed because SubThing didn't override foo.  Per the rules above, 'class' will still report Thing.
I needed to know this (this time) because I wanted to write a rough equivalent to Perl's carp in PHP:
<?php
function carp () {
  $msg = func_get_args();
  if (empty($msg)) $msg = array('warned');
  $bt = debug_backtrace();

  // find nearest site not in our caller's file
  $first_file = $bt[0]['file'];
  $end = count($bt);
  for ($i = 1; $i < $end; ++$i) {
    if ($bt[$i]['file'] != $first_file)
      break;
  }

  if ($i == $end) {
    // not found; try the caller's caller.
    // otherwise we're stuck with our caller.
    $i = ($end > 1 ? 1 : 0);
  }

  error_log(implode(' ', $msg) .
    " at {$bt[$i]['file']}:{$bt[$i]['line']}");
}
Obviously this is a bare-bones approach, and could be adapted to pick different (or report more) stack frames, etc.  But, it Works For Me.™

No comments: