/[drupal]/contributions/modules/devel/devel_themer.module
ViewVC logotype

Contents of /contributions/modules/devel/devel_themer.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.57 - (show annotations) (download) (as text)
Sun Oct 4 03:05:53 2009 UTC (7 weeks, 5 days ago) by davereid
Branch: MAIN
Changes since 1.56: +2 -2 lines
File MIME type: text/x-php
#571566 by cam8001, Dave Reid: Updated for removed drupal_function_exists() and renamed drupal_to_js().
1 <?php
2
3 /**
4 * Implementation of hook_menu().
5 */
6 function devel_themer_menu() {
7 $items = array();
8
9 $items['admin/config/development/devel_themer'] = array(
10 'title' => 'Devel Themer',
11 'description' => 'Display or hide the textual template log',
12 'page callback' => 'drupal_get_form',
13 'page arguments' => array('devel_themer_admin_settings'),
14 'access arguments' => array('administer site configuration'),
15 'type' => MENU_NORMAL_ITEM,
16 );
17 $items['devel_themer/enable'] = array(
18 'title' => 'Devel Themer Enable',
19 'page callback' => 'devel_themer_toggle',
20 'page arguments' => array(1),
21 'access arguments' => array('access devel information'),
22 'type' => MENU_CALLBACK,
23 );
24 $items['devel_themer/disable'] = array(
25 'title' => 'Theme Development Enable',
26 'page callback' => 'devel_themer_toggle',
27 'page arguments' => array(0),
28 'access arguments' => array('access devel information'),
29 'type' => MENU_CALLBACK,
30 );
31 $items['devel_themer/variables'] = array(
32 'title' => 'Theme Development AJAX variables',
33 'page callback' => 'devel_themer_ajax_variables',
34 'access arguments' => array('access devel information'),
35 'type' => MENU_CALLBACK,
36 );
37 return $items;
38 }
39
40 /**
41 * A menu callback used by popup to retrieve variables from cache for a recent page.
42 *
43 * @param $request_id
44 * A unique key that is sent to the browser in Drupal.Settings.devel_themer_request_id
45 * @param $call
46 * The theme call for which you wish to retrieve variables.
47 * @return string
48 * A chunk of HTML with the devel_print_object() rendering of the variables.
49 */
50 function devel_themer_ajax_variables($request_id, $call) {
51 $file = file_directory_path('temporary') . "/devel_themer_$request_id";
52 if ($data = unserialize(file_get_contents($file))) {
53 $variables = $data[$call]['variables'];
54 if (has_krumo()) {
55 print krumo_ob($variables);
56 }
57 elseif ($data[$call]['type'] == 'func') {
58 print devel_print_object($variables, NULL, FALSE);
59 }
60 else {
61 print devel_print_object($variables, '$', FALSE);
62 }
63 }
64 else {
65 print 'Ajax variables file not found. -'. check_plain($file);
66 }
67 $GLOBALS['devel_shutdown'] = FALSE;
68 return;
69 }
70
71 /**
72 * A menu callback. Usually called from the devel block.
73 *
74 * @return void
75 */
76 function devel_themer_toggle($action) {
77 $function = $action == 'enable' ? 'module_enable' : 'module_disable';
78 $$function('devel_themer');
79 drupal_set_message(t('Devel Themer module %action.', array('%action' => $action)));
80 drupal_goto();
81 }
82
83 function devel_themer_admin_settings() {
84 $form['devel_themer_log'] = array('#type' => 'checkbox',
85 '#title' => t('Display theme log'),
86 '#default_value' => variable_get('devel_themer_log', FALSE),
87 '#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.'),
88 );
89 return system_settings_form($form);
90 }
91
92
93 function devel_themer_init() {
94 if (user_access('access devel information')) {
95 $path = drupal_get_path('module', 'devel_themer');
96 // we inject our HTML after page has loaded we have to add this manually.
97 if (has_krumo()) {
98 drupal_add_js($path. '/krumo/krumo.js');
99 drupal_add_css($path. '/krumo/skins/default/skin.css');
100 }
101 drupal_add_css($path .'/devel_themer.css');
102 drupal_add_js($path .'/devel_themer.js');
103
104 // The order these last two are loaded is important.
105 if (module_exists('jquery_ui'))
106 {
107 jquery_ui_add('ui.core');
108 jquery_ui_add('ui.mouse');
109 jquery_ui_add('ui.draggable');
110 }
111 else
112 {
113 // drupal_add_js($path .'/ui.mouse.js');
114 // drupal_add_js($path .'/ui.draggable.js');
115 }
116
117 // This needs to happen after all the other CSS.
118 drupal_add_css('<!--[if IE]>
119 <link href="' . $path .'/devel_themer_ie_fix.css" rel="stylesheet" type="text/css" media="screen" />
120 <![endif]-->', array('type' => 'inline'));
121 devel_themer_popup();
122
123 if (!devel_silent() && variable_get('devel_themer_log', FALSE)) {
124 register_shutdown_function('devel_themer_shutdown');
125 }
126 }
127 }
128
129 function devel_themer_shutdown() {
130 print devel_themer_log();
131 }
132
133 /**
134 * An implementation of hook_theme_registry_alter()
135 * Iterate over theme registry, injecting our catch function into every theme call, including template calls.
136 * The catch function logs theme calls and performs divine nastiness.
137 *
138 * @return void
139 **/
140 function devel_themer_theme_registry_alter($theme_registry) {
141 foreach ($theme_registry as $hook => $data) {
142 if (isset($theme_registry[$hook]['function'])) {
143 // If the hook is a function, store it so it can be run after it has been intercepted.
144 // This does not apply to template calls.
145 $theme_registry[$hook]['devel_function_intercept'] = $theme_registry[$hook]['function'];
146 }
147 // Add our catch function to intercept functions as well as templates.
148 $theme_registry[$hook]['function'] = 'devel_themer_catch_function';
149 }
150 }
151
152 /**
153 * Show all theme templates and functions that could have been used on this page.
154 **/
155 function devel_themer_log() {
156 if (isset($GLOBALS['devel_theme_calls'])) {
157 foreach ($GLOBALS['devel_theme_calls'] as $counter => $call) {
158 // Sometimes $call is a string. Not sure why.
159 if (is_array($call)) {
160 $id = "devel_theme_log_link_$counter";
161 $marker = "<div id=\"$id\" class=\"devel_theme_log_link\"></div>\n";
162
163 $used = $call['used'];
164 if ($call['type'] == 'func') {
165 $name = $call['name']. '()';
166 foreach ($call['candidates'] as $item) {
167 if ($item == $used) {
168 $items[] = "<strong>$used</strong>";
169 }
170 else {
171 $items[] = $item;
172 }
173 }
174 }
175 else {
176 $name = $call['name'];
177 foreach ($call['candidates'] as $item) {
178 if ($item == basename($used)) {
179 $items[] = "<strong>$used</strong>";
180 }
181 else {
182 $items[] = $item;
183 }
184 }
185 }
186 $rows[] = array($call['duration'], $marker. $name, implode(', ', $items));
187 unset($items);
188 }
189 }
190 $header = array('Duration (ms)', 'Template/Function', "Candidate template files or function names");
191 $output = theme('table', $header, $rows);
192 return $output;
193 }
194 }
195
196 // Would be nice if theme() broke this into separate function so we don't copy logic here. this one is better - has cache
197 function devel_themer_get_extension() {
198 global $theme_engine;
199 static $extension = NULL;
200
201 if (!$extension) {
202 $extension_function = $theme_engine .'_extension';
203 if (function_exists($extension_function)) {
204 $extension = $extension_function();
205 }
206 else {
207 $extension = '.tpl.php';
208 }
209 }
210 return $extension;
211 }
212
213 /**
214 * Intercepts all theme calls (including templates), adds to template log, and dispatches to original theme function.
215 * This function gets injected into theme registry in devel_exit().
216 */
217 function devel_themer_catch_function() {
218 $args = func_get_args();
219
220 // Get the function that is normally called.
221 $trace = debug_backtrace();
222 $hook = $trace[2]['args'][0];
223 array_unshift($args, $hook);
224
225 $counter = devel_counter();
226 $timer_name = "thmr_$counter";
227 timer_start($timer_name);
228
229 // The twin of theme(). All rendering done through here.
230 list($return, $meta) = call_user_func_array('devel_themer_theme_twin', $args);
231 $time = timer_stop($timer_name);
232
233 $skip_calls = array('hidden', 'form_element', 'placeholder');
234 if (!empty($return) && !is_array($return) && !is_object($return) && user_access('access devel information')) {
235 list($prefix, $suffix) = devel_theme_call_marker($hook, $counter, 'func');
236 $start_return = substr($return, 0, 31);
237 $start_prefix = substr($prefix, 0, 31);
238
239 if ($start_return != $start_prefix && !in_array($hook, $skip_calls) && empty($GLOBALS['devel_themer_stop'])) {
240 if ($hook == 'page') {
241 $GLOBALS['devel_theme_calls']['page_id'] = $counter;
242 // Stop logging theme calls after we see theme('page'). This prevents
243 // needless logging of devel module's query log, for example. Other modules may set this global as needed.
244 $GLOBALS['devel_themer_stop'] = TRUE;
245 }
246 else {
247 $output = $prefix. "\n ". $return. $suffix. "\n";
248 }
249
250 if ($meta['type'] == 'func') {
251 $name = $meta['used'];
252 $used = $meta['used'];
253 if (empty($meta['wildcards'])) {
254 $meta['wildcards'][$hook] = '';
255 }
256 $candidates = devel_themer_ancestry(array_reverse(array_keys($meta['wildcards'])));
257 if (empty($meta['variables'])) {
258 $variables = array();
259 }
260 }
261 else {
262 $name = $meta['used']. devel_themer_get_extension();
263 if (empty($suggestions)) {
264 array_unshift($meta['suggestions'], $meta['used']);
265 }
266 $candidates = array_reverse(array_map('devel_themer_append_extension', $meta['suggestions']));
267 $used = $meta['template_file'];
268 }
269
270 $key = "thmr_$counter";
271 // This variable gets sent to the browser in Drupal.settings.
272 $GLOBALS['devel_theme_calls'][$key] = array(
273 'name' => $name,
274 'type' => $meta['type'],
275 'duration' => $time['time'],
276 'used' => $used,
277 'candidates' => $candidates,
278 'preprocessors' => isset($meta['preprocessors']) ? $meta['preprocessors'] : array(),
279 );
280
281 // This variable gets serialized and cached on the server.
282 $GLOBALS['devel_themer_server'][$key] = array(
283 'variables' => $meta['variables'],
284 'type' => $meta['type'],
285 );
286 }
287 else {
288 $output = $return;
289 }
290 }
291
292 return isset($output) ? $output : $return;
293 }
294
295 function devel_themer_append_extension($string) {
296 return $string. devel_themer_get_extension();
297 }
298
299 /**
300 * For given theme *function* call, return the ancestry of function names which could have handled the call.
301 * This mimics the way the theme registry is built.
302 *
303 * @param array
304 * A list of theme calls.
305 * @return array()
306 * An array of function names.
307 **/
308 function devel_themer_ancestry($calls) {
309 global $theme, $theme_engine, $base_theme_info;
310 static $prefixes;
311 if (!isset($prefixes)) {
312 $prefixes[] = 'theme';
313 if (isset($base_theme_info)) {
314 foreach ($base_theme_info as $base) {
315 $prefixes[] = $base->name;
316 }
317 }
318 $prefixes[] = $theme_engine;
319 $prefixes[] = $theme;
320 $prefixes = array_filter($prefixes);
321 }
322
323 foreach ($calls as $call) {
324 foreach ($prefixes as $prefix) {
325 $candidates[] = $prefix. '_'. $call;
326 }
327 }
328 return array_reverse($candidates);
329 }
330
331 /**
332 * An unfortunate copy/paste of theme(). This one is called by the devel_themer_catch_function()
333 * and processes all theme calls but gives us info about the candidates, timings, etc. Without this twin,
334 * it was impossible to capture calls to module owned templates (e.g. user_profile) and awkward to determine
335 * which template was finally called and how long it took.
336 *
337 * @return array
338 * A two element array. First element contains the HTML from the theme call. The second contains
339 * a metadata array about the call.
340 *
341 **/
342 function devel_themer_theme_twin() {
343 $args = func_get_args();
344 $hook = array_shift($args);
345
346 static $hooks = NULL;
347 if (!isset($hooks)) {
348 drupal_theme_initialize();
349 $hooks = theme_get_registry();
350 }
351
352 // Gather all possible wildcard functions.
353 $meta['wildcards'] = array();
354 if (is_array($hook)) {
355 foreach ($hook as $candidate) {
356 $meta['wildcards'][$candidate] = FALSE;
357 if (isset($hooks[$candidate])) {
358 $meta['wildcards'][$candidate] = TRUE;
359 break;
360 }
361 }
362 $hook = $candidate;
363 }
364
365 // This should not be needed but some users are getting errors. See http://drupal.org/node/209929
366 if (!isset($hooks[$hook])) {
367 return array('', $meta);
368 }
369
370 $info = $hooks[$hook];
371 $meta['hook'] = $hook;
372 $meta['path'] = $info['theme path'];
373
374 if (isset($info['devel_function_intercept'])) {
375 // The theme call is a function.
376 $output = call_user_func_array($info['devel_function_intercept'], $args);
377 $meta['type'] = 'func';
378 $meta['used'] = $info['devel_function_intercept'];
379 // Try to populate the keys of $args with variable names. Works on PHP5+.
380 if (!empty($args) && class_exists('ReflectionFunction')) {
381 $reflect = new ReflectionFunction($info['devel_function_intercept']);
382 $params = $reflect->getParameters();
383 for ($i=0; $i < count($args); $i++) {
384 // The implementation of the theme function may recieve less parameters than were passed to it.
385 if ($i < count($params)) {
386 $meta['variables'][$params[$i]->getName()] = $args[$i];
387 }
388 else {
389 // @TODO: Consider informing theme developers of theme functions were passed more arguments than
390 // were declared in the functio signature.
391 // This could be disabled by default, with an option to
392 // enable at admin/config/development/devel_themer. Given this is a theme developer module
393 // used by implementors of theme override functions, it would probably be a useful
394 // default feature.
395 $meta['variables'][] = $args[$i];
396 }
397 }
398 }
399 else {
400 $meta['variables'] = $args;
401 }
402 }
403 else {
404 // The theme call is a template.
405 $meta['type'] = 'tpl';
406 $meta['used'] = str_replace($info['theme path'] .'/', '', $info['template']);
407 $variables = array(
408 'template_files' => array()
409 );
410 if (!empty($info['arguments'])) {
411 $count = 0;
412 foreach ($info['arguments'] as $name => $default) {
413 $variables[$name] = isset($args[$count]) ? $args[$count] : $default;
414 $count++;
415 }
416 }
417
418 // default render function and extension.
419 $render_function = 'theme_render_template';
420 $extension = '.tpl.php';
421
422 // Run through the theme engine variables, if necessary
423 global $theme_engine;
424 if (isset($theme_engine)) {
425 // If theme or theme engine is implementing this, it may have
426 // a different extension and a different renderer.
427 if ($info['type'] != 'module') {
428 if (function_exists($theme_engine .'_render_template')) {
429 $render_function = $theme_engine .'_render_template';
430 }
431 $extension_function = $theme_engine .'_extension';
432 if (function_exists($extension_function)) {
433 $extension = $extension_function();
434 }
435 }
436 }
437
438 if (isset($info['preprocess functions']) && is_array($info['preprocess functions'])) {
439 // This construct ensures that we can keep a reference through
440 // call_user_func_array.
441 $args = array(&$variables, $hook);
442 foreach ($info['preprocess functions'] as $preprocess_function) {
443 if (function_exists($preprocess_function)) {
444 call_user_func_array($preprocess_function, $args);
445 }
446 }
447 }
448
449 // Get suggestions for alternate templates out of the variables
450 // that were set. This lets us dynamically choose a template
451 // from a list. The order is FILO, so this array is ordered from
452 // least appropriate first to most appropriate last.
453 $suggestions = array();
454
455 if (isset($variables['template_files'])) {
456 $suggestions = $variables['template_files'];
457 }
458 if (isset($variables['template_file'])) {
459 $suggestions[] = $variables['template_file'];
460 }
461
462 if ($suggestions) {
463 $template_file = drupal_discover_template($info['theme paths'], $suggestions, $extension);
464 }
465
466 if (empty($template_file)) {
467 $template_file = $info['template'] . $extension;
468 if (isset($info['path'])) {
469 $template_file = $info['path'] .'/'. $template_file;
470 }
471 }
472 $output = $render_function($template_file, $variables);
473 $meta['suggestions'] = $suggestions;
474 $meta['template_file'] = $template_file;
475 $meta['variables'] = $variables;
476 $meta['preprocessors'] = $info['preprocess functions'];
477 }
478
479 return array($output, $meta);
480 }
481
482 // We save the huge js array here instead of hook_footer so we can catch theme('page')
483 function devel_themer_exit() {
484 if (!empty($GLOBALS['devel_theme_calls']) && $_SERVER['REQUEST_METHOD'] != 'POST') {
485 // A random string that is sent to the browser. It enables the popup to retrieve params/variables from this request.
486 $request_id = uniqid(rand());
487 // Write the variables information to the a file. It will be retrieved on demand via AJAX.
488 // We used to write this to DB but was getting 'Warning: Got a packet bigger than 'max_allowed_packet' bytes'
489 // Writing to temp dir means we don't worry about folder existence/perms and cleanup is free.
490 file_put_contents(file_directory_path('temporary') . "/devel_themer_$request_id", serialize($GLOBALS['devel_themer_server']));
491
492 $GLOBALS['devel_theme_calls']['request_id'] = $request_id;
493 $GLOBALS['devel_theme_calls']['devel_themer_uri'] = url("devel_themer/variables/$request_id");
494 print '<script type="text/javascript">jQuery.extend(Drupal.settings, '. drupal_json_encode($GLOBALS['devel_theme_calls']) .");</script>\n";
495 }
496 }
497
498 function devel_theme_call_marker($name, $counter, $type) {
499 $id = "thmr_". $counter;
500 return array("<span id=\"$id\" class=\"thmr_call\">", "</span>\n");
501 }
502
503 // just hand out next counter, or return current value
504 function devel_counter($increment = TRUE) {
505 static $counter = 0;
506 if ($increment) {
507 $counter++;
508 }
509 return $counter;
510 }
511
512 /**
513 * Return the popup template
514 * placed here for easy editing
515 */
516 function devel_themer_popup() {
517 $majorver = substr(VERSION, 0, strpos(VERSION, '.'));
518
519 // add translatable strings
520 drupal_add_js(array('thmrStrings' =>
521 array(
522 'themer_info' => t('Themer info'),
523 '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" />',
524 'parents' => t('Parents: '),
525 'function_called' => t('Function called: '),
526 'template_called' => t('Template called: '),
527 'candidate_files' => t('Candidate template files: '),
528 'preprocessors' => t('Preprocess functions: '),
529 'candidate_functions' => t('Candidate function names: '),
530 'drupal_api_docs' => t('link to Drupal API documentation'),
531 'source_link_title' => t('link to source code'),
532 'function_arguments' => t('Function Arguments'),
533 'template_variables' => t('Template Variables'),
534 'file_used' => t('File used: '),
535 'duration' => t('Duration: '),
536 'api_site' => variable_get('devel_api_site', 'http://api.drupal.org/'),
537 'drupal_version' => $majorver,
538 'source_link' => url('devel/source', array('query' => array('file' => ''))),
539 ))
540 , 'setting');
541
542 $title = t('Drupal Themer Information');
543 $intro = t('Click on any element to see information about the Drupal theme function or template that created it.');
544
545 $popup = <<<EOT
546 <div id="themer-fixeder">
547 <div id="themer-relativer">
548 <div id="themer-popup">
549 <div class="topper">
550 <span class="close">X</span> $title
551 </div>
552 <div id="parents" class="row">
553
554 </div>
555 <div class="info row">
556 <div class="starter">$intro</div>
557 <dl>
558 <dt class="key-type">
559
560 </dt>
561 <dd class="key">
562
563 </dd>
564 <div class="used">
565 </div>
566 <dt class="candidates-type">
567
568 </dt>
569 <dd class="candidates">
570
571 </dd>
572
573 <dt class="preprocessors-type">
574
575 </dt>
576 <dd class="preprocessors">
577
578 </dd>
579
580 <div class="duration"></div>
581 </dl>
582 </div><!-- /info -->
583 <div class="attributes row">
584
585 </div><!-- /attributes -->
586 </div><!-- /themer-popup -->
587 </div>
588 </div>
589 EOT;
590
591 drupal_add_js(array('thmr_popup' => $popup), 'setting');
592 }
593
594 /**
595 * Clean up the files we dropped in the temp dir in devel_themer_exit().
596 *
597 * Limitation: one more devel_themer_exit() will run after this function is
598 * called and drop one more file, since hook_exit() is called after the normal
599 * page cycle.
600 *
601 * @return
602 * void.
603 */
604 function devel_themer_cleanup() {
605 foreach (array_keys(file_scan_directory(file_directory_path('temporary'), 'devel_themer_*', array('.', '..', 'CVS'), 0, FALSE)) as $file) {
606 file_delete($file);
607 }
608 }
609
610 /**
611 * Implement hook_cron() for periodic cleanup.
612 *
613 * @return
614 * void.
615 */
616 function devel_themer_cron() {
617 devel_themer_cleanup();
618 }

  ViewVC Help
Powered by ViewVC 1.1.2