devel_themer.module 16.4 KB
Newer Older
1 2
<?php

3 4 5 6 7
/**
 * Implementation of hook_menu().
 */
function devel_themer_menu() {
  $items = array();
8

9 10 11 12 13 14 15 16
  $items['admin/settings/devel_themer'] = array(
    'title' => 'Devel Themer',
    'description' =>  t('Display or hide the textual template log'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('devel_themer_admin_settings'),
    'access arguments' => array('administer site configuration'),
    'type' => MENU_NORMAL_ITEM
  );
17 18 19 20 21 22 23 24
  $items['devel_themer/enable'] = array(
    'title' => 'Devel Themer Enable',
    'page callback' => 'devel_themer_toggle',
    'page arguments' => array(1),
    'access arguments' => array('access devel information'),
    'type' => MENU_CALLBACK,
  );
  $items['devel_themer/disable'] = array(
moshe weitzman's avatar
moshe weitzman committed
25
    'title' => 'Theme Development Enable',
26 27 28 29 30
    'page callback' => 'devel_themer_toggle',
    'page arguments' => array(0),
    'access arguments' => array('access devel information'),
    'type' => MENU_CALLBACK,
  );
31 32 33
  return $items;
}

34 35 36 37 38
/**
 * A menu callback. Usually called from the devel block. 
 * 
 * @return void
 */
moshe weitzman's avatar
moshe weitzman committed
39 40 41 42
function devel_themer_toggle($action) {
  $function = $action == 'enable' ? 'module_enable' : 'module_disable';
  $$function('devel_themer');
  drupal_set_message(t("Devel Themer module $action"));
43
  drupal_goto();
44 45
}

46 47 48 49 50 51 52 53 54 55
function devel_themer_admin_settings() {
  $form['devel_themer_log'] = array('#type' => 'checkbox',
    '#title' => t('Display theme log'),
    '#default_value' => variable_get('devel_themer_log', FALSE),
    '#description' => t('Display the list of theme templates and theme functions which could have been be used for a given page. The one that was actually used is bolded. This is the same data as the represented in the popup, but all calls are listed in chronological order and can alternately be sorted by time.'),
  );
  return system_settings_form($form);
}


56 57 58
function devel_themer_init() {
  if (user_access('access devel information')) {
    $path = drupal_get_path('module', 'devel_themer');
59 60 61 62 63
    // we inject our HTML after page has loaded we have to add this manually.
    if (has_krumo()) {
      drupal_add_js($path. '/krumo/krumo.js');
      drupal_add_css($path. '/krumo/skins/default/skin.css');
    }
64 65
    drupal_add_css($path .'/devel_themer.css');
    drupal_add_js($path .'/devel_themer.js');
66
    drupal_add_js($path .'/jquery-ui-drag.min.js');
67
    // This needs to happen after all the other CSS.
68 69 70
    drupal_set_html_head('<!--[if IE]>
    <link href="' . $path .'/devel_themer_ie_fix.css" rel="stylesheet" type="text/css" media="screen" />
<![endif]-->');
71 72
    devel_themer_popup();

73
    if (!devel_silent() && variable_get('devel_themer_log', FALSE)) {
74 75
      register_shutdown_function('devel_themer_shutdown');
    }
76 77 78
  }
}

79
function devel_themer_shutdown() {
80
  print devel_themer_log();
81 82
}

83 84
/**
 * An implementation of hook_theme_registry_alter()
85
 * Iterate over theme registry, injecting our catch function into every theme call, including template calls.
86
 * The catch function logs theme calls and performs divine nastiness.
87 88 89 90 91
 *
 * @return void
 **/
function devel_themer_theme_registry_alter($theme_registry) {
  foreach ($theme_registry as $hook => $data) {
92 93 94 95 96 97
    if (isset($theme_registry[$hook]['function'])) {
      // If the hook is a function, store it so it can be run after it has been intercepted.
      // This does not apply to template calls.
      $theme_registry[$hook]['devel_function_intercept'] = $theme_registry[$hook]['function'];
    }
    // Add  our catch function to intercept functions as well as templates.
98
    $theme_registry[$hook]['function'] = 'devel_themer_catch_function';
99 100 101 102 103 104
  }
}

/**
 * Show all theme templates and functions that could have been used on this page.
 **/
105
function devel_themer_log() {
106 107 108 109
  if (isset($GLOBALS['devel_theme_calls'])) {
    foreach ($GLOBALS['devel_theme_calls'] as $counter => $call) {
      $id = "devel_theme_log_link_$counter";
      $marker = "<div id=\"$id\" class=\"devel_theme_log_link\"></div>\n";
110

111 112 113 114 115 116 117 118 119
      $used = $call['used'];
      if ($call['type'] == 'func') {
        $name = $call['name']. '()';
        foreach ($call['candidates'] as $candidate) {
          foreach ($candidate as $item) {
            if ($item == $used) {
              $items[] = "<strong>$used</strong>";
            }
            else {
120
              $items[] = $item;
121 122 123 124 125 126 127 128 129
            }
          }
        }
      }
      else {
        $name = $call['name'];
        foreach ($call['candidates'] as $item) {
          if ($item == basename($used)) {
            $items[] = "<strong>$used</strong>";
130 131
          }
          else {
132
            $items[] = $item;
133 134 135
          }
        }
      }
136 137
      $rows[] = array($call['duration'], $marker. $name, implode(', ', $items));
      unset($items);
138
    }
Jeff Robbins's avatar
Jeff Robbins committed
139
    $header = array('Duration (ms)', 'Template/Function', "Candidate template files or function names");
140 141 142 143 144 145
    $output = theme('table', $header, $rows);
    return $output;
  }
}

// Would be nice if theme() broke this into separate function so we don't copy logic here. this one is better - has cache
146
function devel_themer_get_extension() {
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
  global $theme_engine;
  static $extension = NULL;

  if (!$extension) {
    $extension_function = $theme_engine .'_extension';
    if (function_exists($extension_function)) {
      $extension = $extension_function();
    }
    else {
      $extension = '.tpl.php';
    }
  }
  return $extension;
}

/**
163
 * Intercepts all theme calls (including templates), adds to template log, and dispatches to original theme function.
164 165
 * This function gets injected into theme registry in devel_exit().
 */
166
function devel_themer_catch_function() {
167 168 169 170
  $args = func_get_args();

  // Get the function that is normally called.
  $trace = debug_backtrace();
171 172
  $hook = $trace[2]['args'][0];
  array_unshift($args, $hook);
Jeff Robbins's avatar
Jeff Robbins committed
173

174
  $counter = devel_counter();
175 176
  $timer_name = "thmr_$counter";
  timer_start($timer_name);
177 178

  // The twin of theme(). All rendering done through here.
179
  list($return, $meta) = call_user_func_array('devel_themer_theme_twin', $args);
180
  $time = timer_stop($timer_name);
Jeff Robbins's avatar
Jeff Robbins committed
181

182
  $skip = array('hidden', 'form_element', 'placeholder');
183
  if (!empty($return) && !is_array($return) && !is_object($return) && user_access('access devel information')) {
184
    list($prefix, $suffix) = devel_theme_call_marker($hook, $counter, 'func');
185 186 187
    $start_return = substr($return, 0, 31);
    $start_prefix = substr($prefix, 0, 31);

188
    if ($start_return != $start_prefix && !in_array($hook, $skip)) {
189
      $output = $prefix. "\n  ". $return. $suffix. "\n";
Jeff Robbins's avatar
Jeff Robbins committed
190

191 192 193
      if ($meta['type'] == 'func') {
        $name = $meta['used'];
        $used = $meta['used'];
194 195 196 197
        if (empty($meta['wildcards'])) {
          $meta['wildcards'][$hook] = '';
        }  
        $candidates = devel_themer_ancestry(array_reverse(array_keys($meta['wildcards'])));
198 199 200 201
        if (empty($meta['variables'])) {
          $variables = array();
        }
        elseif (has_krumo()) {
202 203 204 205 206
          $variables = krumo_ob($meta['variables']);
        }
        else {
          $variables = devel_print_object($meta['variables'], NULL, FALSE);
        }
207 208
      }
      else {
209
        $name = $meta['hook']. devel_themer_get_extension();
210 211 212 213
        if (empty($suggestions)) {
          array_unshift($meta['suggestions'], $meta['hook']);
        }
        $candidates = array_reverse(array_map('devel_themer_append_extension', $meta['suggestions']));
214
        $used = $meta['template_file'];
215 216 217 218 219 220
        if (has_krumo()) {
          $variables = krumo_ob($meta['variables']);
        }
        else {
          $variables = devel_print_object($meta['variables'], '$', FALSE);
        }
221 222 223 224 225
      }

      $GLOBALS['devel_theme_calls']["thmr_$counter"] = array(
        'name' => $name,
        'type' => $meta['type'],
Jeff Robbins's avatar
Jeff Robbins committed
226
        'duration' => $time['time'],
227 228
        'used' => $used,
        'candidates' => $candidates,
229
        'args' => $variables,
230
      );
231 232 233 234 235
    }
    else {
      $output = $return;
    }
  }
Jeff Robbins's avatar
Jeff Robbins committed
236

237
  return isset($output) ? $output : $return;
238 239
}

240 241 242 243 244 245 246 247
function devel_themer_append_extension($string) {
  return $string. devel_themer_get_extension();
}

/**
 * For  given theme *function* call, return the ancestry of function names which could have handled the call.
 * This mimics the way the theme registry is built.
 *
248 249
 * @param array
 *  A list of theme calls.
250
 * @return array()
251
 *   An array of function names.
252
 **/
253
function devel_themer_ancestry($calls) {
254 255 256 257 258 259 260 261 262 263 264 265 266
  global $theme, $theme_engine, $base_theme_info;
  static $prefixes;
  if (!isset($prefixes)) {
    $prefixes[] = 'theme';
    if (isset($base_theme_info)) {
      foreach ($base_theme_info as $base) {
        $prefixes[] = $base->name;
      }
    }
    $prefixes[] = $theme_engine;
    $prefixes[] = $theme;
    $prefixes = array_filter($prefixes);
  }
267

268 269 270 271
  foreach ($calls as $call) {
    foreach ($prefixes as $prefix) {
      $candidates[] = $prefix. '_'. $call;
    }
272 273 274 275
  }
  return array_reverse($candidates);
}

276 277
/**
 * An unfortunate copy/paste of theme(). This one is called by the devel_themer_catch_function()
Jeff Robbins's avatar
Jeff Robbins committed
278
 * and processes all theme calls but gives us info about the candidates, timings, etc. Without this twin,
279 280 281
 * it was impossible to capture calls to module owned templates (e.g. user_profile) and awkward to determine
 * which template was finally called and how long it took.
 *
282 283 284 285
 * @return array
 *   A two element array. First element contains the HTML from the theme call. The second contains 
 *   a metadata array about the call. 
 *
286 287 288 289 290 291 292 293 294 295
 **/
function devel_themer_theme_twin() {
  $args = func_get_args();
  $hook = array_shift($args);

  static $hooks = NULL;
  if (!isset($hooks)) {
    init_theme();
    $hooks = theme_get_registry();
  }
Jeff Robbins's avatar
Jeff Robbins committed
296

297 298 299 300 301 302 303 304 305 306 307 308
  // Gather all possible wildcard functions.
  $meta['wildcards'] = array();
  if (is_array($hook)) {
    foreach ($hook as $candidate) {
      $meta['wildcards'][$candidate] = FALSE;
      if (isset($hooks[$candidate])) {
        $meta['wildcards'][$candidate] = TRUE;
        break;
      }
    }
    $hook = $candidate;
  }
Jeff Robbins's avatar
Jeff Robbins committed
309

310 311 312 313 314
  // This should not be needed but some users are getting errors. See http://drupal.org/node/209929
  if (!isset($hooks[$hook])) {
    return array('', $meta);
  }

315
  $info = $hooks[$hook];
316
  $meta['hook'] = $hook;
317
  $meta['path'] = $info['theme path'];
318 319 320 321 322 323 324 325 326

  // Include a file if the theme function or preprocess function is held elsewhere.
  if (!empty($info['file'])) {
    $include_file = $info['file'];
    if (isset($info['path'])) {
      $include_file = $info['path'] .'/'. $include_file;
    }
    include_once($include_file);
  }
327
  if (isset($info['devel_function_intercept'])) {
328
    // The theme call is a function.
329
    $output = call_user_func_array($info['devel_function_intercept'], $args);
330
    $meta['type'] = 'func';
331
    $meta['used'] = $info['devel_function_intercept'];
332 333
    // Try to populate the keys of $args with variable names. Works on PHP5+.
    if (!empty($args) && class_exists('ReflectionFunction')) {
334
      $reflect = new ReflectionFunction($info['devel_function_intercept']);
335 336 337 338 339 340 341 342
      $params = $reflect->getParameters();
      for ($i=0; $i < count($args); $i++) {
        $meta['variables'][$params[$i]->getName()] = $args[$i];
      }
    }
    else {
      $meta['variables'] = $args;
    }
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412
  }
  else {
    // The theme call is a template.
    $meta['type'] = 'tpl';
    $variables = array(
      'template_files' => array()
    );
    if (!empty($info['arguments'])) {
      $count = 0;
      foreach ($info['arguments'] as $name => $default) {
        $variables[$name] = isset($args[$count]) ? $args[$count] : $default;
        $count++;
      }
    }

    // default render function and extension.
    $render_function = 'theme_render_template';
    $extension = '.tpl.php';

    // Run through the theme engine variables, if necessary
    global $theme_engine;
    if (isset($theme_engine)) {
      // If theme or theme engine is implementing this, it may have
      // a different extension and a different renderer.
      if ($hooks[$hook]['type'] != 'module') {
        if (function_exists($theme_engine .'_render_template')) {
          $render_function = $theme_engine .'_render_template';
        }
        $extension_function = $theme_engine .'_extension';
        if (function_exists($extension_function)) {
          $extension = $extension_function();
        }
      }
    }

    if (isset($info['preprocess functions']) && is_array($info['preprocess functions'])) {
      // This construct ensures that we can keep a reference through
      // call_user_func_array.
      $args = array(&$variables, $hook);
      foreach ($info['preprocess functions'] as $preprocess_function) {
        if (function_exists($preprocess_function)) {
          call_user_func_array($preprocess_function, $args);
        }
      }
    }

    // Get suggestions for alternate templates out of the variables
    // that were set. This lets us dynamically choose a template
    // from a list. The order is FILO, so this array is ordered from
    // least appropriate first to most appropriate last.
    $suggestions = array();

    if (isset($variables['template_files'])) {
      $suggestions = $variables['template_files'];
    }
    if (isset($variables['template_file'])) {
      $suggestions[] = $variables['template_file'];
    }

    if ($suggestions) {
      $template_file = drupal_discover_template($info['theme paths'], $suggestions, $extension);
    }

    if (empty($template_file)) {
      $template_file = $hooks[$hook]['template'] . $extension;
      if (isset($hooks[$hook]['path'])) {
        $template_file = $hooks[$hook]['path'] .'/'. $template_file;
      }
    }
    $output = $render_function($template_file, $variables);
413
    $meta['suggestions'] = $suggestions;
414
    $meta['template_file'] = $template_file;
Jeff Robbins's avatar
Jeff Robbins committed
415
    $meta['variables'] = $variables;
416
  }
417

418
  return array($output, $meta);
419 420
}

421 422 423 424 425
// we emit the huge js array here instead of hook_footer so we can catch theme('page')
function devel_themer_exit() {
  if (!empty($GLOBALS['devel_theme_calls'])) {
    print '<script type="text/javascript">jQuery.extend(Drupal.settings, '.  drupal_to_js($GLOBALS['devel_theme_calls']) .");</script>\n";
  }
426 427 428 429
}

function devel_theme_call_marker($name, $counter, $type) {
  $id = "thmr_". $counter;
430
  return array("<span id=\"$id\" class=\"thmr_call\">", "</span>\n");
431 432 433 434 435 436 437 438 439
}

// just hand out next counter, or return current value
function devel_counter($increment = TRUE) {
  static $counter = 0;
  if ($increment) {
    $counter++;
  }
  return $counter;
440 441 442 443 444 445 446 447 448 449 450 451
}

/**
 * Return the popup template
 * placed here for easy editing
 */
function devel_themer_popup() {
  $majorver = substr(VERSION, 0, strpos(VERSION, '.'));

  // add translatable strings
  drupal_add_js(array('thmrStrings' =>
    array(
452 453
      'themer_info' => t('Themer info'),
      'toggle_throbber' => ' <img src="'. base_path() . drupal_get_path('module', 'devel'). '/loader-little.gif' .'" alt="'. t('loading') .'" class="throbber" width="16" height="16" style="display:none" />',
454 455 456 457 458 459
      'parents' => t('Parents: '),
      'function_called' => t('Function called: '),
      'template_called' => t('Template called: '),
      'candidate_files' => t('Candidate template files: '),
      'candidate_functions' => t('Candidate function names: '),
      'drupal_api_docs' => t('link to Drupal API documentation'),
460
      'source_link_title' => t('link to source code'),
461 462 463
      'function_arguments' => t('Function Arguments'),
      'template_variables' => t('Template Variables'),
      'file_used' => t('File used: '),
Jeff Robbins's avatar
Jeff Robbins committed
464
      'duration' => t('Duration: '),
465 466
      'api_site' => variable_get('devel_api_site', 'http://api.drupal.org/'),
      'drupal_version' => $majorver,
467
      'source_link' => url('devel/source', array('query' => array('file' => ''))),
468 469 470 471
    ))
    , 'setting');

  $title = t('Drupal Themer Information');
Jeff Robbins's avatar
Jeff Robbins committed
472
  $intro = t('Click on any element to see information about the Drupal theme function or template that created it.');
473 474

  $popup = <<<EOT
475 476
  <div id="themer-fixeder">
  <div id="themer-relativer">
477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492
  <div id="themer-popup">
      <div class="topper">
        <span class="close">X</span> $title
      </div>
      <div id="parents" class="row">

      </div>
      <div class="info row">
        <div class="starter">$intro</div>
        <dl>
          <dt class="key-type">

          </dt>
          <dd class="key">

          </dd>
Jeff Robbins's avatar
Jeff Robbins committed
493 494
          <div class="used">
          </div>
495 496 497 498 499 500
          <dt class="candidates-type">

          </dt>
          <dd class="candidates">

          </dd>
Jeff Robbins's avatar
Jeff Robbins committed
501
          <div class="duration"></div>
502 503 504 505 506 507
        </dl>
      </div><!-- /info -->
      <div class="attributes row">

      </div><!-- /attributes -->
    </div><!-- /themer-popup -->
508 509
    </div>
    </div>
510 511 512
EOT;

  drupal_add_js(array('thmr_popup' => $popup), 'setting');
513
}