Namespaces

  • Nette
    • Application
      • Diagnostics
      • Responses
      • Routers
      • UI
    • Caching
      • Storages
    • ComponentModel
    • Config
    • Database
      • Diagnostics
      • Drivers
      • Reflection
      • Table
    • DI
    • Diagnostics
    • Forms
      • Controls
      • Rendering
    • Http
    • Iterators
    • Latte
      • Macros
    • Loaders
    • Localization
    • Mail
    • Reflection
    • Security
    • Templating
    • Utils
  • NetteModule
  • None
  • PHP

Classes

  • Debugger
  • FireLogger
  • Logger

Interfaces

  • IBarPanel
  • Overview
  • Namespace
  • Class
  • Tree
  1: <?php
  2: 
  3: /**
  4:  * This file is part of the Nette Framework (http://nette.org)
  5:  *
  6:  * Copyright (c) 2004, 2011 David Grudl (http://davidgrudl.com)
  7:  *
  8:  * For the full copyright and license information, please view
  9:  * the file license.txt that was distributed with this source code.
 10:  */
 11: 
 12: namespace Nette\Diagnostics;
 13: 
 14: use Nette;
 15: 
 16: 
 17: 
 18: /**
 19:  * Debugger: displays and logs errors.
 20:  *
 21:  * Behavior is determined by two factors: mode & output
 22:  * - modes: production / development
 23:  * - output: HTML / AJAX / CLI / other (e.g. XML)
 24:  *
 25:  * @author     David Grudl
 26:  */
 27: final class Debugger
 28: {
 29:     /** @var bool in production mode is suppressed any debugging output */
 30:     public static $productionMode;
 31: 
 32:     /** @var bool in console mode is omitted HTML output */
 33:     public static $consoleMode;
 34: 
 35:     /** @var int timestamp with microseconds of the start of the request */
 36:     public static $time;
 37: 
 38:     /** @var bool is AJAX request detected? */
 39:     private static $ajaxDetected;
 40: 
 41:     /** @var string  requested URI or command line */
 42:     public static $source;
 43: 
 44:     /** @var string URL pattern mask to open editor */
 45:     public static $editor = 'editor://open/?file=%file&line=%line';
 46: 
 47:     /********************* Debugger::dump() ****************d*g**/
 48: 
 49:     /** @var int  how many nested levels of array/object properties display {@link Debugger::dump()} */
 50:     public static $maxDepth = 3;
 51: 
 52:     /** @var int  how long strings display {@link Debugger::dump()} */
 53:     public static $maxLen = 150;
 54: 
 55:     /** @var bool display location? {@link Debugger::dump()} */
 56:     public static $showLocation = FALSE;
 57: 
 58:     /********************* errors and exceptions reporting ****************d*g**/
 59: 
 60:     /** server modes {@link Debugger::enable()} */
 61:     const DEVELOPMENT = FALSE,
 62:         PRODUCTION = TRUE,
 63:         DETECT = NULL;
 64: 
 65:     /** @var BlueScreen */
 66:     public static $blueScreen;
 67: 
 68:     /** @var bool determines whether any error will cause immediate death */
 69:     public static $strictMode = FALSE; // $immediateDeath
 70: 
 71:     /** @var bool disables the @ (shut-up) operator so that notices and warnings are no longer hidden */
 72:     public static $scream = FALSE;
 73: 
 74:     /** @var array of callbacks specifies the functions that are automatically called after fatal error */
 75:     public static $onFatalError = array();
 76: 
 77:     /** @var bool {@link Debugger::enable()} */
 78:     private static $enabled = FALSE;
 79: 
 80:     /** @var mixed {@link Debugger::tryError()} FALSE means catching is disabled */
 81:     private static $lastError = FALSE;
 82: 
 83:     /********************* logging ****************d*g**/
 84: 
 85:     /** @var Logger */
 86:     public static $logger;
 87: 
 88:     /** @var FireLogger */
 89:     public static $fireLogger;
 90: 
 91:     /** @var string name of the directory where errors should be logged; FALSE means that logging is disabled */
 92:     public static $logDirectory;
 93: 
 94:     /** @var string email to sent error notifications */
 95:     public static $email;
 96: 
 97:     /** @deprecated */
 98:     public static $mailer;
 99: 
100:     /** @deprecated */
101:     public static $emailSnooze;
102: 
103:     /********************* debug bar ****************d*g**/
104: 
105:     /** @var Bar */
106:     public static $bar;
107: 
108:     /** @var DefaultBarPanel */
109:     private static $errorPanel;
110: 
111:     /** @var DefaultBarPanel */
112:     private static $dumpPanel;
113: 
114:     /********************* Firebug extension ****************d*g**/
115: 
116:     /** {@link Debugger::log()} and {@link Debugger::fireLog()} */
117:     const DEBUG = 'debug',
118:         INFO = 'info',
119:         WARNING = 'warning',
120:         ERROR = 'error',
121:         CRITICAL = 'critical';
122: 
123: 
124: 
125:     /**
126:      * Static class - cannot be instantiated.
127:      */
128:     final public function __construct()
129:     {
130:         throw new Nette\StaticClassException;
131:     }
132: 
133: 
134: 
135:     /**
136:      * Static class constructor.
137:      * @internal
138:      */
139:     public static function _init()
140:     {
141:         self::$time = microtime(TRUE);
142:         self::$consoleMode = PHP_SAPI === 'cli';
143:         self::$productionMode = self::DETECT;
144:         if (self::$consoleMode) {
145:             self::$source = empty($_SERVER['argv']) ? 'cli' : 'cli: ' . implode(' ', $_SERVER['argv']);
146:         } else {
147:             self::$ajaxDetected = isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest';
148:             if (isset($_SERVER['REQUEST_URI'])) {
149:                 self::$source = (isset($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off') ? 'https://' : 'http://')
150:                     . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : ''))
151:                     . $_SERVER['REQUEST_URI'];
152:             }
153:         }
154: 
155:         self::$logger = new Logger;
156:         self::$logDirectory = & self::$logger->directory;
157:         self::$email = & self::$logger->email;
158:         self::$mailer = & self::$logger->mailer;
159:         self::$emailSnooze = & Logger::$emailSnooze;
160: 
161:         self::$fireLogger = new FireLogger;
162: 
163:         self::$blueScreen = new BlueScreen;
164:         self::$blueScreen->addPanel(function($e) {
165:             if ($e instanceof Nette\Templating\FilterException) {
166:                 return array(
167:                     'tab' => 'Template',
168:                     'panel' => '<p><b>File:</b> ' . Helpers::editorLink($e->sourceFile, $e->sourceLine)
169:                     . '&nbsp; <b>Line:</b> ' . ($e->sourceLine ? $e->sourceLine : 'n/a') . '</p>'
170:                     . ($e->sourceLine ? '<pre>' . BlueScreen::highlightFile($e->sourceFile, $e->sourceLine) . '</pre>' : '')
171:                 );
172:             }
173:         });
174: 
175:         self::$bar = new Bar;
176:         self::$bar->addPanel(new DefaultBarPanel('time'));
177:         self::$bar->addPanel(new DefaultBarPanel('memory'));
178:         self::$bar->addPanel(self::$errorPanel = new DefaultBarPanel('errors')); // filled by _errorHandler()
179:         self::$bar->addPanel(self::$dumpPanel = new DefaultBarPanel('dumps')); // filled by barDump()
180:     }
181: 
182: 
183: 
184:     /********************* errors and exceptions reporting ****************d*g**/
185: 
186: 
187: 
188:     /**
189:      * Enables displaying or logging errors and exceptions.
190:      * @param  mixed         production, development mode, autodetection or IP address(es) whitelist.
191:      * @param  string        error log directory; enables logging in production mode, FALSE means that logging is disabled
192:      * @param  string        administrator email; enables email sending in production mode
193:      * @return void
194:      */
195:     public static function enable($mode = NULL, $logDirectory = NULL, $email = NULL)
196:     {
197:         error_reporting(E_ALL | E_STRICT);
198: 
199:         // production/development mode detection
200:         if (is_bool($mode)) {
201:             self::$productionMode = $mode;
202: 
203:         } elseif (is_string($mode)) { // IP addresses
204:             $mode = preg_split('#[,\s]+#', "$mode 127.0.0.1 ::1");
205:         }
206: 
207:         if (is_array($mode)) { // IP addresses whitelist detection
208:             self::$productionMode = !isset($_SERVER['REMOTE_ADDR']) || !in_array($_SERVER['REMOTE_ADDR'], $mode, TRUE);
209:         }
210: 
211:         if (self::$productionMode === self::DETECT) {
212:             if (isset($_SERVER['SERVER_ADDR']) || isset($_SERVER['LOCAL_ADDR'])) { // IP address based detection
213:                 $addrs = array();
214:                 if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { // proxy server detected
215:                     $addrs = preg_split('#,\s*#', $_SERVER['HTTP_X_FORWARDED_FOR']);
216:                 }
217:                 if (isset($_SERVER['REMOTE_ADDR'])) {
218:                     $addrs[] = $_SERVER['REMOTE_ADDR'];
219:                 }
220:                 $addrs[] = isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : $_SERVER['LOCAL_ADDR'];
221:                 self::$productionMode = FALSE;
222:                 foreach ($addrs as $addr) {
223:                     $oct = explode('.', $addr);
224:                     if ($addr !== '::1' && (count($oct) !== 4 || ($oct[0] !== '10' && $oct[0] !== '127' && ($oct[0] !== '172' || $oct[1] < 16 || $oct[1] > 31)
225:                         && ($oct[0] !== '169' || $oct[1] !== '254') && ($oct[0] !== '192' || $oct[1] !== '168')))
226:                     ) {
227:                         self::$productionMode = TRUE;
228:                         break;
229:                     }
230:                 }
231: 
232:             } else {
233:                 self::$productionMode = !self::$consoleMode;
234:             }
235:         }
236: 
237:         // logging configuration
238:         if (is_string($logDirectory)) {
239:             self::$logDirectory = realpath($logDirectory);
240:             if (self::$logDirectory === FALSE) {
241:                 throw new Nette\DirectoryNotFoundException("Directory '$logDirectory' is not found.");
242:             }
243:         } elseif ($logDirectory === FALSE) {
244:             self::$logDirectory = FALSE;
245: 
246:         } elseif (self::$logDirectory === NULL) {
247:             self::$logDirectory = defined('APP_DIR') ? APP_DIR . '/../log' : getcwd() . '/log';
248:         }
249:         if (self::$logDirectory) {
250:             ini_set('error_log', self::$logDirectory . '/php_error.log');
251:         }
252: 
253:         // php configuration
254:         if (function_exists('ini_set')) {
255:             ini_set('display_errors', !self::$productionMode); // or 'stderr'
256:             ini_set('html_errors', FALSE);
257:             ini_set('log_errors', FALSE);
258: 
259:         } elseif (ini_get('display_errors') != !self::$productionMode && ini_get('display_errors') !== (self::$productionMode ? 'stderr' : 'stdout')) { // intentionally ==
260:             throw new Nette\NotSupportedException('Function ini_set() must be enabled.');
261:         }
262: 
263:         if ($email) {
264:             if (!is_string($email)) {
265:                 throw new Nette\InvalidArgumentException('Email address must be a string.');
266:             }
267:             self::$email = $email;
268:         }
269: 
270:         if (!defined('E_DEPRECATED')) {
271:             define('E_DEPRECATED', 8192);
272:         }
273: 
274:         if (!defined('E_USER_DEPRECATED')) {
275:             define('E_USER_DEPRECATED', 16384);
276:         }
277: 
278:         if (!self::$enabled) {
279:             register_shutdown_function(array(__CLASS__, '_shutdownHandler'));
280:             set_exception_handler(array(__CLASS__, '_exceptionHandler'));
281:             set_error_handler(array(__CLASS__, '_errorHandler'));
282:             self::$enabled = TRUE;
283:         }
284:     }
285: 
286: 
287: 
288:     /**
289:      * Is Debug enabled?
290:      * @return bool
291:      */
292:     public static function isEnabled()
293:     {
294:         return self::$enabled;
295:     }
296: 
297: 
298: 
299:     /**
300:      * Logs message or exception to file (if not disabled) and sends email notification (if enabled).
301:      * @param  string|Exception
302:      * @param  int  one of constant Debugger::INFO, WARNING, ERROR (sends email), CRITICAL (sends email)
303:      * @return void
304:      */
305:     public static function log($message, $priority = self::INFO)
306:     {
307:         if (self::$logDirectory === FALSE) {
308:             return;
309: 
310:         } elseif (!self::$logDirectory) {
311:             throw new Nette\InvalidStateException('Logging directory is not specified in Nette\Diagnostics\Debugger::$logDirectory.');
312:         }
313: 
314:         if ($message instanceof \Exception) {
315:             $exception = $message;
316:             $message = "PHP Fatal error: "
317:                 . ($message instanceof Nette\FatalErrorException
318:                     ? $exception->getMessage()
319:                     : "Uncaught exception " . get_class($exception) . " with message '" . $exception->getMessage() . "'")
320:                 . " in " . $exception->getFile() . ":" . $exception->getLine();
321: 
322:             $hash = md5($exception );
323:             $exceptionFilename = "exception " . @date('Y-m-d H-i-s') . " $hash.html";
324:             foreach (new \DirectoryIterator(self::$logDirectory) as $entry) {
325:                 if (strpos($entry, $hash)) {
326:                     $exceptionFilename = NULL; break;
327:                 }
328:             }
329:         }
330: 
331:         self::$logger->log(array(
332:             @date('[Y-m-d H-i-s]'),
333:             $message,
334:             self::$source ? ' @  ' . self::$source : NULL,
335:             !empty($exceptionFilename) ? ' @@  ' . $exceptionFilename : NULL
336:         ), $priority);
337: 
338:         if (!empty($exceptionFilename) && $logHandle = @fopen(self::$logDirectory . '/'. $exceptionFilename, 'w')) {
339:             ob_start(); // double buffer prevents sending HTTP headers in some PHP
340:             ob_start(function($buffer) use ($logHandle) { fwrite($logHandle, $buffer); }, 1);
341:             self::$blueScreen->render($exception);
342:             ob_end_flush();
343:             ob_end_clean();
344:             fclose($logHandle);
345:         }
346:     }
347: 
348: 
349: 
350:     /**
351:      * Shutdown handler to catch fatal errors and execute of the planned activities.
352:      * @return void
353:      * @internal
354:      */
355:     public static function _shutdownHandler()
356:     {
357:         if (!self::$enabled) {
358:             return;
359:         }
360: 
361:         // fatal error handler
362:         static $types = array(
363:             E_ERROR => 1,
364:             E_CORE_ERROR => 1,
365:             E_COMPILE_ERROR => 1,
366:             E_PARSE => 1,
367:         );
368:         $error = error_get_last();
369:         if (isset($types[$error['type']])) {
370:             self::_exceptionHandler(new Nette\FatalErrorException($error['message'], 0, $error['type'], $error['file'], $error['line'], NULL));
371:         }
372: 
373:         // debug bar (require HTML & development mode)
374:         if (self::$bar && !self::$productionMode && self::isHtmlMode()) {
375:             self::$bar->render();
376:         }
377:     }
378: 
379: 
380: 
381:     /**
382:      * Handler to catch uncaught exception.
383:      * @param  \Exception
384:      * @return void
385:      * @internal
386:      */
387:     public static function _exceptionHandler(\Exception $exception)
388:     {
389:         if (!headers_sent()) { // for PHP < 5.2.4
390:             header('HTTP/1.1 500 Internal Server Error');
391:         }
392: 
393:         try {
394:             if (self::$productionMode) {
395:                 self::log($exception, self::ERROR);
396: 
397:                 if (self::$consoleMode) {
398:                     echo "ERROR: the server encountered an internal error and was unable to complete your request.\n";
399: 
400:                 } elseif (self::isHtmlMode()) {
401:                     require __DIR__ . '/templates/error.phtml';
402:                 }
403: 
404:             } else {
405:                 if (self::$consoleMode) { // dump to console
406:                     echo "$exception\n";
407: 
408:                 } elseif (self::isHtmlMode()) { // dump to browser
409:                     self::$blueScreen->render($exception);
410:                     if (self::$bar) {
411:                         self::$bar->render();
412:                     }
413: 
414:                 } elseif (!self::fireLog($exception, self::ERROR)) { // AJAX or non-HTML mode
415:                     self::log($exception);
416:                 }
417:             }
418: 
419:             foreach (self::$onFatalError as $handler) {
420:                 call_user_func($handler, $exception);
421:             }
422:         } catch (\Exception $e) {
423:             echo "\nNette\\Debug FATAL ERROR: thrown ", get_class($e), ': ', $e->getMessage(),
424:                 "\nwhile processing ", get_class($exception), ': ', $exception->getMessage(), "\n";
425:         }
426:         self::$enabled = FALSE; // un-register shutdown function
427:         exit(255);
428:     }
429: 
430: 
431: 
432:     /**
433:      * Handler to catch warnings and notices.
434:      * @param  int    level of the error raised
435:      * @param  string error message
436:      * @param  string file that the error was raised in
437:      * @param  int    line number the error was raised at
438:      * @param  array  an array of variables that existed in the scope the error was triggered in
439:      * @return bool   FALSE to call normal error handler, NULL otherwise
440:      * @throws Nette\FatalErrorException
441:      * @internal
442:      */
443:     public static function _errorHandler($severity, $message, $file, $line, $context)
444:     {
445:         if (self::$scream) {
446:             error_reporting(E_ALL | E_STRICT);
447:         }
448: 
449:         if (self::$lastError !== FALSE && ($severity & error_reporting()) === $severity) { // tryError mode
450:             self::$lastError = new \ErrorException($message, 0, $severity, $file, $line);
451:             return NULL;
452:         }
453: 
454:         if ($severity === E_RECOVERABLE_ERROR || $severity === E_USER_ERROR) {
455:             throw new Nette\FatalErrorException($message, 0, $severity, $file, $line, $context);
456: 
457:         } elseif (($severity & error_reporting()) !== $severity) {
458:             return FALSE; // calls normal error handler to fill-in error_get_last()
459: 
460:         } elseif (self::$strictMode && !self::$productionMode) {
461:             self::_exceptionHandler(new Nette\FatalErrorException($message, 0, $severity, $file, $line, $context));
462:         }
463: 
464:         static $types = array(
465:             E_WARNING => 'Warning',
466:             E_COMPILE_WARNING => 'Warning', // currently unable to handle
467:             E_USER_WARNING => 'Warning',
468:             E_NOTICE => 'Notice',
469:             E_USER_NOTICE => 'Notice',
470:             E_STRICT => 'Strict standards',
471:             E_DEPRECATED => 'Deprecated',
472:             E_USER_DEPRECATED => 'Deprecated',
473:         );
474: 
475:         $message = 'PHP ' . (isset($types[$severity]) ? $types[$severity] : 'Unknown error') . ": $message";
476:         $count = & self::$errorPanel->data["$message|$file|$line"];
477: 
478:         if ($count++) { // repeated error
479:             return NULL;
480: 
481:         } elseif (self::$productionMode) {
482:             self::log("$message in $file:$line", self::ERROR);
483:             return NULL;
484: 
485:         } else {
486:             $ok = self::fireLog(new \ErrorException($message, 0, $severity, $file, $line), self::WARNING);
487:             return !self::isHtmlMode() || (!self::$bar && !$ok) ? FALSE : NULL;
488:         }
489: 
490:         return FALSE; // call normal error handler
491:     }
492: 
493: 
494: 
495:     /**
496:      * Handles exception thrown in __toString().
497:      * @param  \Exception
498:      * @return void
499:      */
500:     public static function toStringException(\Exception $exception)
501:     {
502:         if (self::$enabled) {
503:             self::_exceptionHandler($exception);
504:         } else {
505:             trigger_error($exception->getMessage(), E_USER_ERROR);
506:         }
507:     }
508: 
509: 
510: 
511:     /**
512:      * Starts catching potential errors/warnings.
513:      * @return void
514:      */
515:     public static function tryError()
516:     {
517:         if (!self::$enabled && self::$lastError === FALSE) {
518:             set_error_handler(array(__CLASS__, '_errorHandler'));
519:         }
520:         self::$lastError = NULL;
521:     }
522: 
523: 
524: 
525:     /**
526:      * Returns catched error/warning message.
527:      * @param  \ErrorException  catched error
528:      * @return bool
529:      */
530:     public static function catchError(& $error)
531:     {
532:         if (!self::$enabled && self::$lastError !== FALSE) {
533:             restore_error_handler();
534:         }
535:         $error = self::$lastError;
536:         self::$lastError = FALSE;
537:         return (bool) $error;
538:     }
539: 
540: 
541: 
542:     /********************* useful tools ****************d*g**/
543: 
544: 
545: 
546:     /**
547:      * Dumps information about a variable in readable format.
548:      * @param  mixed  variable to dump
549:      * @param  bool   return output instead of printing it? (bypasses $productionMode)
550:      * @return mixed  variable itself or dump
551:      */
552:     public static function dump($var, $return = FALSE)
553:     {
554:         if (!$return && self::$productionMode) {
555:             return $var;
556:         }
557: 
558:         $output = "<pre class=\"nette-dump\">" . Helpers::htmlDump($var) . "</pre>\n";
559: 
560:         if (!$return) {
561:             $trace = debug_backtrace();
562:             $i = !isset($trace[1]['class']) && isset($trace[1]['function']) && $trace[1]['function'] === 'dump' ? 1 : 0;
563:             if (isset($trace[$i]['file'], $trace[$i]['line']) && is_file($trace[$i]['file'])) {
564:                 $lines = file($trace[$i]['file']);
565:                 preg_match('#dump\((.*)\)#', $lines[$trace[$i]['line'] - 1], $m);
566:                 $output = substr_replace(
567:                     $output,
568:                     ' title="' . htmlspecialchars((isset($m[0]) ? "$m[0] \n" : '') . "in file {$trace[$i]['file']} on line {$trace[$i]['line']}") . '"',
569:                     4, 0);
570: 
571:                 if (self::$showLocation) {
572:                     $output = substr_replace(
573:                         $output,
574:                         ' <small>in ' . Helpers::editorLink($trace[$i]['file'], $trace[$i]['line']) . ":{$trace[$i]['line']}</small>",
575:                         -8, 0);
576:                 }
577:             }
578:         }
579: 
580:         if (self::$consoleMode) {
581:             $output = htmlspecialchars_decode(strip_tags($output), ENT_NOQUOTES);
582:         }
583: 
584:         if ($return) {
585:             return $output;
586: 
587:         } else {
588:             echo $output;
589:             return $var;
590:         }
591:     }
592: 
593: 
594: 
595:     /**
596:      * Starts/stops stopwatch.
597:      * @param  string  name
598:      * @return float   elapsed seconds
599:      */
600:     public static function timer($name = NULL)
601:     {
602:         static $time = array();
603:         $now = microtime(TRUE);
604:         $delta = isset($time[$name]) ? $now - $time[$name] : 0;
605:         $time[$name] = $now;
606:         return $delta;
607:     }
608: 
609: 
610: 
611:     /**
612:      * Dumps information about a variable in Nette Debug Bar.
613:      * @param  mixed  variable to dump
614:      * @param  string optional title
615:      * @return mixed  variable itself
616:      */
617:     public static function barDump($var, $title = NULL)
618:     {
619:         if (!self::$productionMode) {
620:             $dump = array();
621:             foreach ((is_array($var) ? $var : array('' => $var)) as $key => $val) {
622:                 $dump[$key] = Helpers::clickableDump($val);
623:             }
624:             self::$dumpPanel->data[] = array('title' => $title, 'dump' => $dump);
625:         }
626:         return $var;
627:     }
628: 
629: 
630: 
631:     /**
632:      * Sends message to FireLogger console.
633:      * @param  mixed   message to log
634:      * @return bool    was successful?
635:      */
636:     public static function fireLog($message)
637:     {
638:         if (!self::$productionMode) {
639:             return self::$fireLogger->log($message);
640:         }
641:     }
642: 
643: 
644: 
645:     private static function isHtmlMode()
646:     {
647:         return !self::$ajaxDetected && !self::$consoleMode
648:             && !preg_match('#^Content-Type: (?!text/html)#im', implode("\n", headers_list()));
649:     }
650: 
651: 
652: 
653:     /** @deprecated */
654:     public static function addPanel(IBarPanel $panel, $id = NULL)
655:     {
656:         self::$bar->addPanel($panel, $id);
657:     }
658: 
659: }
660: 
Nette Framework 2.0beta1 API API documentation generated by ApiGen 2.3.0