ErrorHandler.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2009, Union of RAD (http://union-of-rad.org)
  6. * @license http://opensource.org/licenses/bsd-license.php The BSD License
  7. */
  8. namespace lithium\core;
  9. use Exception;
  10. use ErrorException;
  11. use lithium\util\collection\Filters;
  12. /**
  13. * The `ErrorHandler` class allows PHP errors and exceptions to be handled in a uniform way. Using
  14. * the `ErrorHandler`'s configuration, it is possible to have very broad but very tight control
  15. * over error handling in your application.
  16. *
  17. * {{{ embed:lithium\tests\cases\core\ErrorHandlerTest::testExceptionCatching(2-7) }}}
  18. *
  19. * Using a series of cascading rules and handlers, it is possible to capture and handle very
  20. * specific errors and exceptions.
  21. */
  22. class ErrorHandler extends \lithium\core\StaticObject {
  23. /**
  24. * Configuration parameters.
  25. *
  26. * @var array Config params
  27. */
  28. protected static $_config = array();
  29. /**
  30. * Types of checks available for sorting & parsing errors/exceptions.
  31. * Default checks are for `code`, `stack` and `message`.
  32. *
  33. * @var array Array of checks represented as closures, indexed by name.
  34. */
  35. protected static $_checks = array();
  36. /**
  37. * Currently registered exception handler.
  38. *
  39. * @var closure Closure representing exception handler.
  40. */
  41. protected static $_exceptionHandler = null;
  42. /**
  43. * State of error/exception handling.
  44. *
  45. * @var boolean True if custom error/exception handlers have been registered, false
  46. * otherwise.
  47. */
  48. protected static $_isRunning = false;
  49. protected static $_runOptions = array();
  50. /**
  51. * Setup basic error handling checks/types, as well as register the error and exception
  52. * handlers.
  53. *
  54. * Called on static class initialization (i.e. when first loaded).
  55. *
  56. * @return void
  57. */
  58. public static function __init() {
  59. static::$_checks = array(
  60. 'type' => function($config, $info) {
  61. return (boolean) array_filter((array) $config['type'], function($type) use ($info) {
  62. return $type === $info['type'] || is_subclass_of($info['type'], $type);
  63. });
  64. },
  65. 'code' => function($config, $info) {
  66. return ($config['code'] & $info['code']);
  67. },
  68. 'stack' => function($config, $info) {
  69. return (boolean) array_intersect((array) $config['stack'], $info['stack']);
  70. },
  71. 'message' => function($config, $info) {
  72. return preg_match($config['message'], $info['message']);
  73. }
  74. );
  75. $self = get_called_class();
  76. static::$_exceptionHandler = function($exception, $return = false) use ($self) {
  77. if (ob_get_length()) {
  78. ob_end_clean();
  79. }
  80. $info = compact('exception') + array(
  81. 'type' => get_class($exception),
  82. 'stack' => $self::trace($exception->getTrace())
  83. );
  84. foreach (array('message', 'file', 'line', 'trace') as $key) {
  85. $method = 'get' . ucfirst($key);
  86. $info[$key] = $exception->{$method}();
  87. }
  88. return $return ? $info : $self::handle($info);
  89. };
  90. }
  91. /**
  92. * Configure the `ErrorHandler`.
  93. *
  94. * @param array $config Configuration directives.
  95. * @return Current configuration set.
  96. */
  97. public static function config($config = array()) {
  98. return (static::$_config = array_merge($config, static::$_config));
  99. }
  100. /**
  101. * Register error and exception handlers.
  102. *
  103. * This method (`ErrorHandler::run()`) needs to be called as early as possible in the bootstrap
  104. * cycle; immediately after `require`-ing `bootstrap/libraries.php` is your best bet.
  105. *
  106. * @param array $config The configuration with which to start the error handler. Available
  107. * options include:
  108. * - `'trapErrors'` _boolean_: Defaults to `false`. If set to `true`, PHP errors
  109. * will be caught by `ErrorHandler` and handled in-place. Execution will resume
  110. * in the same context in which the error occurred.
  111. * - `'convertErrors'` _boolean_: Defaults to `true`, and specifies that all PHP
  112. * errors should be converted to `ErrorException`s and thrown from the point
  113. * where the error occurred. The exception will be caught at the first point in
  114. * the stack trace inside a matching `try`/`catch` block, or that has a matching
  115. * error handler applied using the `apply()` method.
  116. * @return void
  117. */
  118. public static function run(array $config = array()) {
  119. $defaults = array('trapErrors' => false, 'convertErrors' => true);
  120. if (static::$_isRunning) {
  121. return;
  122. }
  123. static::$_isRunning = true;
  124. static::$_runOptions = $config + $defaults;
  125. $self = get_called_class();
  126. $trap = function($code, $message, $file, $line = 0, $context = null) use ($self) {
  127. $trace = debug_backtrace();
  128. $trace = array_slice($trace, 1, count($trace));
  129. $self::handle(compact('type', 'code', 'message', 'file', 'line', 'trace', 'context'));
  130. };
  131. $convert = function($code, $message, $file, $line = 0, $context = null) use ($self) {
  132. throw new ErrorException($message, 500, $code, $file, $line);
  133. };
  134. if (static::$_runOptions['trapErrors']) {
  135. set_error_handler($trap);
  136. } elseif (static::$_runOptions['convertErrors']) {
  137. set_error_handler($convert);
  138. }
  139. set_exception_handler(static::$_exceptionHandler);
  140. }
  141. /**
  142. * Returns the state of the `ErrorHandler`, indicating whether or not custom error/exception
  143. * handers have been regsitered.
  144. *
  145. * @return void
  146. */
  147. public static function isRunning() {
  148. return static::$_isRunning;
  149. }
  150. /**
  151. * Unooks `ErrorHandler`'s exception and error handlers, and restores PHP's defaults. May have
  152. * unexpected results if it is not matched with a prior call to `run()`, or if other error
  153. * handlers are set after a call to `run()`.
  154. *
  155. * @return void
  156. */
  157. public static function stop() {
  158. restore_error_handler();
  159. restore_exception_handler();
  160. static::$_isRunning = false;
  161. }
  162. /**
  163. * Wipes out all configuration and resets the error handler to its initial state when loaded.
  164. * Mainly used for testing.
  165. *
  166. * @return void
  167. */
  168. public static function reset() {
  169. static::$_config = array();
  170. static::$_checks = array();
  171. static::$_exceptionHandler = null;
  172. static::__init();
  173. }
  174. /**
  175. * Receives the handled errors and exceptions that have been caught, and processes them
  176. * in a normalized manner.
  177. *
  178. * @param object|array $info
  179. * @param array $scope
  180. * @return boolean True if successfully handled, false otherwise.
  181. */
  182. public static function handle($info, $scope = array()) {
  183. $checks = static::$_checks;
  184. $rules = $scope ?: static::$_config;
  185. $handler = static::$_exceptionHandler;
  186. $info = is_object($info) ? $handler($info, true) : $info;
  187. $defaults = array(
  188. 'type' => null, 'code' => 0, 'message' => null, 'file' => null, 'line' => 0,
  189. 'trace' => array(), 'context' => null, 'exception' => null
  190. );
  191. $info = (array) $info + $defaults;
  192. $info['stack'] = static::trace($info['trace']);
  193. $info['origin'] = static::_origin($info['trace']);
  194. foreach ($rules as $config) {
  195. foreach (array_keys($config) as $key) {
  196. if ($key === 'conditions' || $key === 'scope' || $key === 'handler') {
  197. continue;
  198. }
  199. if (!isset($info[$key]) || !isset($checks[$key])) {
  200. continue 2;
  201. }
  202. if (($check = $checks[$key]) && !$check($config, $info)) {
  203. continue 2;
  204. }
  205. }
  206. if (!isset($config['handler'])) {
  207. return false;
  208. }
  209. if ((isset($config['conditions']) && $call = $config['conditions']) && !$call($info)) {
  210. return false;
  211. }
  212. if ((isset($config['scope'])) && static::handle($info, $config['scope']) !== false) {
  213. return true;
  214. }
  215. $handler = $config['handler'];
  216. return $handler($info) !== false;
  217. }
  218. return false;
  219. }
  220. /**
  221. * Determine frame from the stack trace where the error/exception was first generated.
  222. *
  223. * @param array $stack Stack trace from error/exception that was produced.
  224. * @return string Class where error/exception was generated.
  225. */
  226. protected static function _origin(array $stack) {
  227. foreach ($stack as $frame) {
  228. if (isset($frame['class'])) {
  229. return trim($frame['class'], '\\');
  230. }
  231. }
  232. }
  233. public static function apply($object, array $conditions, $handler) {
  234. $conditions = $conditions ?: array('type' => 'Exception');
  235. list($class, $method) = is_string($object) ? explode('::', $object) : $object;
  236. $wrap = static::$_exceptionHandler;
  237. $_self = get_called_class();
  238. $filter = function($self, $params, $chain) use ($_self, $conditions, $handler, $wrap) {
  239. try {
  240. return $chain->next($self, $params, $chain);
  241. } catch (Exception $e) {
  242. if (!$_self::matches($e, $conditions)) {
  243. throw $e;
  244. }
  245. return $handler($wrap($e, true), $params);
  246. }
  247. };
  248. if (is_string($class)) {
  249. Filters::apply($class, $method, $filter);
  250. } else {
  251. $class->applyFilter($method, $filter);
  252. }
  253. }
  254. public static function matches($info, $conditions) {
  255. $checks = static::$_checks;
  256. $handler = static::$_exceptionHandler;
  257. $info = is_object($info) ? $handler($info, true) : $info;
  258. foreach (array_keys($conditions) as $key) {
  259. if ($key === 'conditions' || $key === 'scope' || $key === 'handler') {
  260. continue;
  261. }
  262. if (!isset($info[$key]) || !isset($checks[$key])) {
  263. return false;
  264. }
  265. if (($check = $checks[$key]) && !$check($conditions, $info)) {
  266. return false;
  267. }
  268. }
  269. if ((isset($config['conditions']) && $call = $config['conditions']) && !$call($info)) {
  270. return false;
  271. }
  272. return true;
  273. }
  274. /**
  275. * Trim down a typical stack trace to class & method calls.
  276. *
  277. * @param array $stack A `debug_backtrace()`-compatible stack trace output.
  278. * @return array Returns a flat stack array containing class and method references.
  279. */
  280. public static function trace(array $stack) {
  281. $result = array();
  282. foreach ($stack as $frame) {
  283. if (isset($frame['function'])) {
  284. if (isset($frame['class'])) {
  285. $result[] = trim($frame['class'], '\\') . '::' . $frame['function'];
  286. } else {
  287. $result[] = $frame['function'];
  288. }
  289. }
  290. }
  291. return $result;
  292. }
  293. }
  294. ?>