Browse Source

Merge pull request #5285 from cakephp/improved-error-pages

WIP - 3.0 Improved error pages
José Lorenzo Rodríguez 11 years ago
parent
commit
3cb4d484fe

+ 2 - 1
src/Error/Debugger.php

@@ -402,7 +402,8 @@ class Debugger {
 		if (!isset($data[$line])) {
 			return $lines;
 		}
-		for ($i = $line - ($context + 1); $i < $line + $context; $i++) {
+		$line = $line - 1;
+		for ($i = $line - $context; $i < $line + $context + 1; $i++) {
 			if (!isset($data[$i])) {
 				continue;
 			}

+ 31 - 42
src/Template/Element/exception_stack_trace.ctp

@@ -16,58 +16,47 @@
  */
 use Cake\Error\Debugger;
 ?>
-<h3>Stack Trace</h3>
-<ul class="cake-stack-trace">
-<?php foreach ($error->getTrace() as $i => $stack): ?>
-	<li><?php
-	$excerpt = $arguments = '';
-	$params = array();
+
+<?php
+foreach ($error->getTrace() as $i => $stack):
+	$excerpt = $params = [];
 
 	if (isset($stack['file']) && isset($stack['line'])):
-		printf(
-			'<a href="#" onclick="traceToggle(event, \'file-excerpt-%s\')">%s line %s</a>',
-			$i,
-			Debugger::trimPath($stack['file']),
-			$stack['line']
-		);
-		$excerpt = sprintf('<div id="file-excerpt-%s" class="cake-code-dump" style="display:none;"><pre>', $i);
-		$excerpt .= implode("\n", Debugger::excerpt($stack['file'], $stack['line'] - 1, 2));
-		$excerpt .= '</pre></div> ';
+		$excerpt = Debugger::excerpt($stack['file'], $stack['line'], 4);
+	endif;
+
+	if (isset($stack['file'])):
+		$file = $stack['file'];
 	else:
-		echo '<a href="#">[internal function]</a>';
+		$file = '[internal function]';
 	endif;
-	echo ' &rarr; ';
+
 	if ($stack['function']):
-		$args = array();
 		if (!empty($stack['args'])):
 			foreach ((array)$stack['args'] as $arg):
-				$args[] = Debugger::getType($arg);
 				$params[] = Debugger::exportVar($arg, 4);
 			endforeach;
+		else:
+			$params[] = 'No arguments';
 		endif;
+	endif;
+?>
+	<div id="stack-frame-<?= $i ?>" style="display:none;" class="stack-details">
+		<span class="stack-frame-file"><?= h($file) ?></span>
+		<a href="#" class="toggle-link stack-frame-args" data-target="stack-args-<?= $i ?>">toggle arguments</a>
 
-		$called = isset($stack['class']) ? $stack['class'] . $stack['type'] . $stack['function'] : $stack['function'];
+		<table class="code-excerpt" cellspacing="0" cellpadding="0">
+		<?php $lineno = isset($stack['line']) ? $stack['line'] - 4 : 0 ?>
+		<?php foreach ($excerpt as $l => $line): ?>
+			<tr>
+				<td class="excerpt-number" data-number="<?= $lineno + $l ?>"></td>
+				<td class="excerpt-line"><?= $line ?></td>
+			</tr>
+		<?php endforeach; ?>
+		</table>
 
-		printf(
-			'<a href="#" onclick="traceToggle(event, \'trace-args-%s\')">%s(%s)</a> ',
-			$i,
-			$called,
-			h(implode(', ', $args))
-		);
-		$arguments = sprintf('<div id="trace-args-%s" class="cake-code-dump" style="display: none;"><pre>', $i);
-		$arguments .= h(implode("\n", $params));
-		$arguments .= '</pre></div>';
-	endif;
-	echo $excerpt;
-	echo $arguments;
-	?></li>
+		<div id="stack-args-<?= $i ?>" style="display: none;">
+			<pre><?= implode("\n", $params) ?></pre>
+		</div>
+	</div>
 <?php endforeach; ?>
-</ul>
-<script type="text/javascript">
-function traceToggle(event, id) {
-	var el = document.getElementById(id);
-	el.style.display = (el.style.display === 'block') ? 'none' : 'block';
-	event.preventDefault();
-	return false;
-}
-</script>

+ 39 - 0
src/Template/Element/exception_stack_trace_nav.ctp

@@ -0,0 +1,39 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.0.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+use Cake\Error\Debugger;
+?>
+<a href="#" class="toggle-link toggle-vendor-frames">toggle vendor stack frames</a>
+
+<ul class="stack-trace">
+<?php foreach ($error->getTrace() as $i => $stack): ?>
+	<?php $class = (isset($stack['file']) && strpos(APP, $stack['file']) === false) ? 'vendor-frame' : 'app-frame'; ?>
+	<li class="stack-frame <?= $class ?>">
+	<?php if (isset($stack['file']) && isset($stack['line'])): ?>
+		<a href="#" data-target="stack-frame-<?= $i ?>">
+			<?php if (!isset($stack['class'])): ?>
+				<span class="stack-function">&rang; <?= h($stack['function']) ?></span>
+			<?php else: ?>
+				<span class="stack-function">&rang; <?= h($stack['class']) ?>::<?= h($stack['function']) ?></span>
+			<?php endif; ?>
+			<span class="stack-file">
+				<?= h(Debugger::trimPath($stack['file'])) ?>, line <?= $stack['line'] ?>
+			</span>
+		</a>
+	<?php else: ?>
+		<a href="#">&rang; [internal function]</a>
+	<?php endif; ?>
+	</li>
+<?php endforeach; ?>
+</ul>

+ 24 - 18
src/Template/Error/missing_action.ctp

@@ -31,32 +31,38 @@ if (empty($plugin)) {
 } else {
 	$path = Plugin::classPath($plugin) . 'Controller' . DS . $prefix . h($controller) . '.php';
 }
+
+$this->layout = 'dev_error';
+
+$this->assign('title', sprintf('Missing Method in %s', h($controller)));
+$this->assign(
+	'subheading',
+	sprintf('The action <em>%s</em> is not defined in <em>%s</em>', h($action), h($controller))
+);
+$this->assign('templateName', 'missing_action.ctp');
+
+$this->start('file');
 ?>
-<h2><?= sprintf('Missing Method in %s', h($controller)); ?></h2> <p class="error">
-	<strong>Error: </strong>
-	<?= sprintf('The action <em>%s</em> is not defined in controller <em>%s</em>', h($action), h($controller)); ?>
-</p>
 <p class="error">
 	<strong>Error: </strong>
 	<?= sprintf('Create <em>%s::%s()</em> in file: %s.', h($controller),  h($action), $path); ?>
 </p>
-<pre>
-&lt;?php
-namespace <?= h($namespace); ?>\Controller<?= h($prefixNs); ?>;
 
-use <?= h($namespace); ?>\Controller\AppController;
+<?php
+$code = <<<PHP
+<?php
+namespace {$namespace}\Controller{$prefixNs};
+
+use {$namespace}\Controller\AppController;
 
-class <?= h($controller); ?> extends AppController {
+class {$controller} extends AppController {
 
-<strong>
-	public function <?= h($action); ?>() {
+	public function {$action}() {
 
 	}
-</strong>
 }
-</pre>
-<p class="notice">
-	<strong>Notice: </strong>
-	<?= sprintf('If you want to customize this error message, create %s', APP_DIR . DS . 'Template' . DS . 'Error' . DS . 'missing_action.ctp'); ?>
-</p>
-<?= $this->element('exception_stack_trace'); ?>
+PHP;
+?>
+
+<div class="code-dump"><?php highlight_string($code) ?></div>
+<?php $this->end() ?>

+ 283 - 0
src/Template/Layout/dev_error.ctp

@@ -0,0 +1,283 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.0.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+use Cake\Error\Debugger;
+?>
+<!DOCTYPE html>
+<html>
+<head>
+	<?= $this->Html->charset() ?>
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+	<title>
+		Error: <?= $this->fetch('title') ?>
+	</title>
+	<?= $this->Html->meta('icon') ?>
+</head>
+<body>
+	<header>
+		<h1 class="header-title">
+			<?= $this->fetch('title') ?>
+			<span class="header-type"><?= get_class($error) ?></span>
+		</h1>
+		<div class="header-help">
+			<a target="_blank" href="http://book.cakephp.org/3.0/">Documentation</a>
+			<a target="_blank" href="http://api.cakephp.org/3.0/">API</a>
+		</div>
+	</header>
+
+	<div class="error-contents">
+		<p class="error-subheading">
+			<?= $this->fetch('subheading') ?>
+		</p>
+
+		<?= $this->element('exception_stack_trace'); ?>
+
+		<div class="error-suggestion">
+			<?= $this->fetch('file') ?>
+		</div>
+
+		<?php if ($this->fetch('templateName')): ?>
+		<p class="notice">
+			If you want to customize this error message, create
+			<em><?= APP_DIR . DS . 'Template' . DS . 'Error' . DS . $this->fetch('templateName') ?></em>
+		</p>
+		<?php endif; ?>
+	</div>
+
+	<div class="error-nav">
+		<?= $this->element('exception_stack_trace_nav') ?>
+	</div>
+
+<script type="text/javascript">
+function bindEvent(selector, eventName, listener) {
+	var els = document.querySelectorAll(selector);
+	for (var i = 0, len = els.length; i < len; i++) {
+		els[i].addEventListener(eventName, listener, false);
+	}
+}
+
+function toggleElement(el) {
+	if (el.style.display === 'none') {
+		el.style.display = 'block';
+	} else {
+		el.style.display = 'none';
+	}
+}
+
+function each(els, cb) {
+	var i, len;
+	for (i = 0, len = els.length; i < len; i++) {
+		cb(els[i], i);
+	}
+}
+
+window.addEventListener('load', function() {
+	bindEvent('.stack-frame-args', 'click', function(event) {
+		var target = this.dataset['target'];
+		var el = document.getElementById(target);
+		toggleElement(el);
+		event.preventDefault();
+	});
+
+	var details = document.querySelectorAll('.stack-details');
+	var frames = document.querySelectorAll('.stack-frame');
+	bindEvent('.stack-frame a', 'click', function(event) {
+		each(frames, function(el) {
+			el.classList.remove('active');
+		});
+		this.parentNode.classList.add('active');
+
+		each(details, function(el) {
+			el.style.display = 'none';
+		});
+
+		var target = document.getElementById(this.dataset['target']);
+		toggleElement(target);
+		event.preventDefault();
+	});
+
+	bindEvent('.toggle-vendor-frames', 'click', function(event) {
+		each(frames, function(el) {
+			if (el.classList.contains('vendor-frame')) {
+				toggleElement(el);
+			}
+		});
+		event.preventDefault();
+	});
+});
+</script>
+
+<style>
+body {
+	font: 14px helvetica, arial, sans-serif;
+	color: #222;
+	background-color: #f8f8f8;
+	padding:0;
+	margin: 0;
+	max-height: 100%;
+}
+
+.code-dump,
+pre {
+	background: #fefefe;
+	border: 1px solid #ddd;
+	padding: 5px;
+}
+
+header {
+	background-color: #C3232D;
+	color: #ffffff;
+	padding: 16px 10px;
+	border-bottom: 3px solid #626262;
+}
+.header-title {
+	margin: 0;
+	font-weight: normal;
+	font-size: 30px;
+	line-height: 64px;
+}
+.header-type {
+	opacity: 0.75;
+	display: block;
+	font-size: 16px;
+	line-height: 1;
+}
+.header-help {
+	font-size: 12px;
+	line-height: 1;
+	position: absolute;
+	top: 30px;
+	right: 16px;
+}
+.header-help a {
+	color: #fff;
+}
+
+.error-nav {
+	float: left;
+	width: 30%;
+}
+.error-contents {
+	padding: 10px 1%;
+	float: right;
+	width: 68%;
+}
+
+.error,
+.error-subheading {
+	font-size: 18px;
+	margin-top: 0;
+	padding: 10px;
+	border: 1px solid #EDBD26;
+}
+.error-subheading {
+	background: #1798A5;
+	color: #fff;
+	border: 1px solid #02808C;
+}
+.error {
+	background: #ffd54f;
+}
+
+.stack-trace {
+	list-style: none;
+	margin: 0;
+	padding: 0;
+}
+.stack-frame {
+	padding: 10px;
+	border-bottom: 1px solid #212121;
+}
+.stack-frame:last-child {
+	border-bottom: none;
+}
+.stack-frame a {
+	display: block;
+	color: #212121;
+	text-decoration: none;
+}
+.stack-frame.active {
+	background: #e5e5e5;
+}
+.stack-frame a:hover {
+	text-decoration: underline;
+}
+.stack-file,
+.stack-function {
+	display: block;
+	margin-bottom: 5px;
+}
+
+.stack-frame-file,
+.stack-file {
+	font-family: consolas, monospace;
+}
+.stack-function {
+	font-weight: bold;
+}
+.stack-file {
+	font-size: 0.9em;
+}
+
+.stack-details {
+	background: #ececec;
+	box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
+	border: 1px solid #ababab;
+	padding: 10px;
+	margin-bottom: 18px;
+}
+.stack-frame-args {
+	float: right;
+}
+
+.toggle-link {
+	color: #1798A5;
+	text-decoration: none;
+}
+.toggle-link:hover {
+	text-decoration: underline;
+}
+.toggle-vendor-frames {
+	padding: 5px;
+	display: block;
+	text-align: center;
+}
+
+.code-excerpt {
+	width: 100%;
+	margin: 5px 0;
+	background: #fefefe;
+}
+.code-highlight {
+	display: block;
+	background: #fff59d;
+}
+.excerpt-line {
+	padding-left: 2px;
+}
+.excerpt-number {
+	background: #f6f6f6;
+	width: 50px;
+	text-align: right;
+	color: #666;
+	border-right: 1px solid #ddd;
+	padding: 2px;
+}
+.excerpt-number:after {
+	content: attr(data-number);
+}
+</style>
+
+</body>
+</html>

+ 9 - 3
tests/TestCase/Error/DebuggerTest.php

@@ -91,24 +91,30 @@ class DebuggerTest extends TestCase {
 	public function testExcerpt() {
 		$result = Debugger::excerpt(__FILE__, __LINE__, 2);
 		$this->assertTrue(is_array($result));
-		$this->assertEquals(5, count($result));
+		$this->assertCount(5, $result);
 		$this->assertRegExp('/function(.+)testExcerpt/', $result[1]);
 
 		$result = Debugger::excerpt(__FILE__, 2, 2);
 		$this->assertTrue(is_array($result));
-		$this->assertEquals(4, count($result));
+		$this->assertCount(4, $result);
 
 		$pattern = '/<code>.*?<span style\="color\: \#\d+">.*?&lt;\?php/';
 		$this->assertRegExp($pattern, $result[0]);
 
 		$result = Debugger::excerpt(__FILE__, 11, 2);
-		$this->assertEquals(5, count($result));
+		$this->assertCount(5, $result);
 
 		$pattern = '/<span style\="color\: \#\d{6}">\*<\/span>/';
 		$this->assertRegExp($pattern, $result[0]);
 
 		$return = Debugger::excerpt('[internal]', 2, 2);
 		$this->assertTrue(empty($return));
+
+		$result = Debugger::excerpt(__FILE__, __LINE__, 5);
+		$this->assertCount(11, $result);
+		$this->assertContains('Debugger', $result[5]);
+		$this->assertContains('excerpt', $result[5]);
+		$this->assertContains('__FILE__', $result[5]);
 	}
 
 /**

+ 1 - 1
tests/TestCase/Error/ExceptionRendererTest.php

@@ -484,7 +484,7 @@ class ExceptionRendererTest extends TestCase {
 					'plugin' => '',
 				)),
 				array(
-					'/<h2>Missing Method in PostsController<\/h2>/',
+					'/Missing Method in PostsController/',
 					'/<em>PostsController::index\(\)<\/em>/'
 				),
 				404