Newer
Older
Moshe Weitzman
committed
<?php
/**
* Implementation of hook_menu().
*/
function devel_themer_menu() {
$items = array();
$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
);
$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(
'title' => 'Devel Themer Enable',
'page callback' => 'devel_themer_toggle',
'page arguments' => array(0),
'access arguments' => array('access devel information'),
'type' => MENU_CALLBACK,
);
function devel_themer_toggle($value) {
$_SESSION['devel_themer_toggle'] = (int) $value;
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);
}
function devel_themer_init() {
if (user_access('access devel information')) {
$path = drupal_get_path('module', 'devel_themer');
drupal_add_css($path .'/devel_themer.css');
drupal_add_js($path .'/devel_themer.js');
drupal_add_js($path .'/jquery-ui-drag.min.js');
// these needs to happen after all the other CSS
drupal_set_html_head('<!--[if IE]>
<link href="' . $path .'/devel_themer_ie_fix.css" rel="stylesheet" type="text/css" media="screen" />
<![endif]-->');
devel_themer_popup();
if (!devel_silent() && variable_get('devel_themer_log', FALSE)) {
register_shutdown_function('devel_themer_shutdown');
}
function devel_themer_shutdown() {
Moshe Weitzman
committed
/**
* An implementation of hook_theme_registry_alter()
* Iterate over theme registry, injecting our catch function into every theme function.
* The catch function logs theme calls and performs divine nastiness.
Moshe Weitzman
committed
*
* @return void
**/
function devel_themer_theme_registry_alter($theme_registry) {
foreach ($theme_registry as $hook => $data) {
// if (isset($data['function'])) {
Moshe Weitzman
committed
// Copy over original registry of the hook so it can be caught later.
$theme_registry[$hook]['devel_themer'] = $theme_registry[$hook];
// Replace with our catch function.
$theme_registry[$hook]['function'] = 'devel_themer_catch_function';
Moshe Weitzman
committed
$theme_registry[$hook]['type'] = 'module';
$theme_registry[$hook]['theme path'] = drupal_get_path('module', 'devel_themer');
Moshe Weitzman
committed
}
}
/**
* Show all theme templates and functions that could have been used on this page.
**/
Moshe Weitzman
committed
$extension = devel_get_theme_extension();
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";
Moshe Weitzman
committed
if (count($call['candidates']) > 1) {
$used = $call['used'];
foreach ($call['candidates'] as $key => $value) {
$test = $value == basename($used);
Moshe Weitzman
committed
}
else {
$test = $value == $used;
}
$call['candidates'][$key] = $test ? "<strong>$used</strong>" : $value;
}
}
$name = $call['type'] == 'func' ? $call['name']. '()' : $call['name']. $extension;
$rows[] = array($call['time'], $marker. $name, implode(', ', $call['candidates']));
Moshe Weitzman
committed
}
$header = array('Time (ms)', 'Template/Function', "Candidate template files or function names");
Moshe Weitzman
committed
$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
function devel_get_theme_extension() {
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;
}
/**
* Log template file suggestions into a $GLOBAL.
*/
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
// function devel_themer_preprocess($vars, $hook) {
// $counter = devel_counter();
// $GLOBALS['devel_theme_calls'][$counter] = array(
// 'name' => $hook,
// 'type' => 'tpl',
// 'candidates' => $vars['template_files'],
// );
// // add in a 'template' file if it was declared
// if (isset($vars['template_file'])) {
// array_unshift($GLOBALS['devel_theme_calls'][$counter]['candidates'] = $vars['template_file']);
// }
// // add the plain template name
// array_unshift($GLOBALS['devel_theme_calls'][$counter]['candidates'], $hook);
// }
//
// // A wrapper function so we can know what template actually got called. Log that info in the GLOBAL for later display.
// // I found no other easy way to determine this. Patches welcome.
// function phptemplate_render_template($file, $variables) {
// $counter = devel_counter(FALSE);
//
// $timer_name = "thmr_$counter";
// timer_start($timer_name);
// $output = theme_render_template($file, $variables);
// $time = timer_stop($timer_name);
// $GLOBALS['devel_theme_calls'][$counter]['time'] = $time['time'];
//
// $GLOBALS['devel_theme_calls'][$counter]['used'] = $file;
// $GLOBALS['devel_theme_calls'][$counter]['args'] = $variables;
//
// // awful attempt to get position #2 in the assoc array $variables
// $i=0;
// foreach ($variables as $key => $var) {
// if ($i == 1) {
// $name = $key;
// }
// $i++;
// }
// list($prefix, $suffix) = devel_theme_call_marker($name, $counter, 'tpl');
// drupal_add_js(array("thmr_$counter" => array('arguments' => devel_print_object($variables, '$', FALSE), 'candidates' => array_reverse($GLOBALS['devel_theme_calls'][$counter]['candidates']), 'used' => $GLOBALS['devel_theme_calls'][$counter]['used'])), 'setting', 'header', FALSE, FALSE, FALSE);
// return $prefix. "\n ". $output. "\n". $suffix. "\n";
// }
Moshe Weitzman
committed
/**
* Intercepts all theme calls (including templates), adds to template log, and dispatches to original theme function.
Moshe Weitzman
committed
* This function gets injected into theme registry in devel_exit().
*/
function devel_themer_catch_function() {
Moshe Weitzman
committed
$args = func_get_args();
// Get the function that is normally called.
$trace = debug_backtrace();
$hook = $trace[2]['args'][0];
array_unshift($args, $hook);
Moshe Weitzman
committed
$counter = devel_counter();
$timer_name = "thmr_$counter";
timer_start($timer_name);
/**
* The twin of theme(). All rendering done through here.
*/
list($return, $meta) = call_user_func_array('devel_themer_theme_twin', $args);
$time = timer_stop($timer_name);
list($prefix, $suffix) = devel_theme_call_marker($hook, $counter, 'func');
Moshe Weitzman
committed
$start_return = substr($return, 0, 31);
$start_prefix = substr($prefix, 0, 31);
$skip = array('theme_hidden');
Moshe Weitzman
committed
// Pass the call to the original function. Wrap as needed.
if ($start_return != $start_prefix && !in_array($hook, $skip)) {
Moshe Weitzman
committed
$output = $prefix. "\n ". $return. $suffix. "\n";
// TODO: this is fishy
// drupal_add_js(array("thmr_$counter" => array('args' => devel_print_object($args, NULL, FALSE), 'candidates' => $candidates)), 'setting', 'header', FALSE, FALSE, FALSE);
Moshe Weitzman
committed
}
else {
$output = $return;
}
}
if ($meta['type'] == 'func') {
$name = $meta['used'];
$used = $meta['used'];
$candidates = array_keys($meta['wildcards']);
$args = devel_print_object($meta['variables'], NULL, FALSE);
}
else {
$name = $meta['hook'];
$candidates = isset($meta['template_files']) ? array_keys($meta['template_files']) : array();
$used = $meta['template_file'];
devel_print_object($meta['variables'], '$', FALSE);
Moshe Weitzman
committed
// Log the call
$GLOBALS['devel_theme_calls']["thmr_$counter"] = array(
Moshe Weitzman
committed
'name' => $name,
'empty' => empty($return),
'time' => $time['time'],
'used' => $used,
'candidates' => $candidates,
// 'suggestions' => isset($suggestions) ? $suggestions : array(),
Moshe Weitzman
committed
);
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
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
return isset($output) ? $output : '';
}
/**
* An unfortunate copy/paste of theme(). This one is called by the devel_themer_catch_function()
* and processes all theme calls but gives us info about the candidates, timings, etc. Without this twin,
* 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.
*
**/
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();
}
// 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;
}
$meta['hook'] = $hook;
// extract the original registry info that we set aside in the devel element.
$info = $hooks[$hook]['devel_themer'];
global $theme_path;
$temp = $theme_path;
$meta['path'] = $theme_path;
// point path_to_theme() to the currently used theme path:
// MW: hope this isn't needed
// $theme_path = $info[$hook]['theme path'];
// 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);
}
if (isset($info['function'])) {
// The theme call is a function.
$output = call_user_func_array($info['function'], $args);
$meta['type'] = 'func';
$meta['used'] = $info['function'];
$meta['variables'] = $args;
}
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();
}
}
}
$meta['extension'] = $extension;
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);
}
}
// $meta['preprocess functions'] = $info['preprocess functions'];
}
// 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);
// Log all the candidate files and note which was actually used.
foreach ($suggestions as $candidate) {
$meta['template_files'][$candidate . $extension] = FALSE;
if ($candidate . $extension == $template_file) {
$meta['template_files'][$candidate . $extension] = TRUE;
}
elseif (file_exists(path_to_theme() .'/'. $candidate . $extension)) {
$meta['template_files'][$candidate . $extension] = NULL;
}
}
}
if (empty($template_file)) {
$template_file = $hooks[$hook]['template'] . $extension;
$meta['template_files'][$template_file] = TRUE;
if (isset($hooks[$hook]['path'])) {
$template_file = $hooks[$hook]['path'] .'/'. $template_file;
}
}
$output = $render_function($template_file, $variables);
$meta['template_file'] = $template_file;
$meta['variables'] = array_keys($variables);
}
// restore path_to_theme()
// $theme_path = $temp;
return array($output, $meta);
Moshe Weitzman
committed
}
/**
* An implementation of hook_footer(). Emit huge js array for the benefit of the popup.
// function devel_themer_footer() {
// drupal_add_js(array("devel_themer" => $GLOBALS['devel_theme_calls']), 'setting', 'footer', FALSE, FALSE, FALSE);
// }
// 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";
}
}
function devel_theme_call_marker($name, $counter, $type) {
$id = "thmr_". $counter;
return array("<span id=\"$id\" thmr_key=\"$name\" thmr_type=\"$type\" class=\"thmr_call\">", "</span>\n");
Moshe Weitzman
committed
}
// just hand out next counter, or return current value
function devel_counter($increment = TRUE) {
static $counter = 0;
if ($increment) {
$counter++;
}
return $counter;
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
}
/**
* 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(
'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'),
'function_arguments' => t('Function Arguments'),
'template_variables' => t('Template Variables'),
'file_used' => t('File used: '),
'api_site' => variable_get('devel_api_site', 'http://api.drupal.org/'),
'drupal_version' => $majorver,
))
, 'setting');
$title = t('Drupal Themer Information');
$intro = t('Click on any element on the page to see the Drupal theme function or template that created it.');
$popup = <<<EOT
<div id="themer-fixeder">
<div id="themer-relativer">
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
<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>
<dt class="candidates-type">
</dt>
<dd class="candidates">
</dd>
<div class="used">
</div>
</dl>
</div><!-- /info -->
<div class="attributes row">
</div><!-- /attributes -->
</div><!-- /themer-popup -->
</div>
</div>
EOT;
drupal_add_js(array('thmr_popup' => $popup), 'setting');