ErrorTrap.php 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. <?php
  2. declare(strict_types=1);
  3. namespace Cake\Error;
  4. use Cake\Core\Configure;
  5. use Cake\Core\InstanceConfigTrait;
  6. use Cake\Error\Renderer\ConsoleErrorRenderer;
  7. use Cake\Error\Renderer\HtmlErrorRenderer;
  8. use Cake\Event\EventDispatcherTrait;
  9. use Exception;
  10. use InvalidArgumentException;
  11. /**
  12. * Entry point to CakePHP's error handling.
  13. *
  14. * Using the `register()` method you can attach an ErrorTrap to PHP's default error handler.
  15. *
  16. * When errors are trapped, errors are logged (if logging is enabled). Then the `Error.beforeRender` event is triggered.
  17. * Finally, errors are 'rendered' using the defined renderer. If no error renderer is defined in configuration
  18. * one of the default implementations will be chosen based on the PHP SAPI.
  19. */
  20. class ErrorTrap
  21. {
  22. use EventDispatcherTrait;
  23. use InstanceConfigTrait {
  24. getConfig as private _getConfig;
  25. }
  26. /**
  27. * See the `Error` key in you `config/app.php`
  28. * for details on the keys and their values.
  29. *
  30. * @var array<string, mixed>
  31. */
  32. protected $_defaultConfig = [
  33. 'errorLevel' => E_ALL,
  34. 'ignoredDeprecationPaths' => [],
  35. 'log' => true,
  36. 'logger' => ErrorLogger::class,
  37. 'errorRenderer' => null,
  38. 'extraFatalErrorMemory' => 4 * 1024,
  39. 'trace' => false,
  40. ];
  41. /**
  42. * Constructor
  43. *
  44. * @param array<string, mixed> $options An options array. See $_defaultConfig.
  45. */
  46. public function __construct(array $options = [])
  47. {
  48. $this->setConfig($options);
  49. }
  50. /**
  51. * Choose an error renderer based on config or the SAPI
  52. *
  53. * @return class-string<\Cake\Error\ErrorRendererInterface>
  54. */
  55. protected function chooseErrorRenderer(): string
  56. {
  57. $config = $this->_getConfig('errorRenderer');
  58. if ($config !== null) {
  59. return $config;
  60. }
  61. /** @var class-string<\Cake\Error\ErrorRendererInterface> */
  62. return PHP_SAPI === 'cli' ? ConsoleErrorRenderer::class : HtmlErrorRenderer::class;
  63. }
  64. /**
  65. * Attach this ErrorTrap to PHP's default error handler.
  66. *
  67. * This will replace the existing error handler, and the
  68. * previous error handler will be discarded.
  69. *
  70. * This method will also set the global error level
  71. * via error_reporting().
  72. *
  73. * @return void
  74. */
  75. public function register(): void
  76. {
  77. $level = $this->_config['errorLevel'] ?? -1;
  78. error_reporting($level);
  79. set_error_handler([$this, 'handleError'], $level);
  80. }
  81. /**
  82. * Handle an error from PHP set_error_handler
  83. *
  84. * Will use the configured renderer to generate output
  85. * and output it.
  86. *
  87. * This method will dispatch the `Error.beforeRender` event which can be listened
  88. * to on the global event manager.
  89. *
  90. * @param int $code Code of error
  91. * @param string $description Error description
  92. * @param string|null $file File on which error occurred
  93. * @param int|null $line Line that triggered the error
  94. * @return bool True if error was handled
  95. */
  96. public function handleError(
  97. int $code,
  98. string $description,
  99. ?string $file = null,
  100. ?int $line = null
  101. ): bool {
  102. if (!(error_reporting() & $code)) {
  103. return false;
  104. }
  105. if ($code === E_USER_ERROR || $code === E_ERROR || $code === E_PARSE) {
  106. throw new FatalErrorException($description, $code, $file, $line);
  107. }
  108. /** @var array $trace */
  109. $trace = Debugger::trace(['start' => 1, 'format' => 'points']);
  110. $error = new PhpError($code, $description, $file, $line, $trace);
  111. $debug = Configure::read('debug');
  112. $renderer = $this->renderer();
  113. $logger = $this->logger();
  114. try {
  115. // Log first incase rendering or event listeners fail
  116. $logger->logMessage($error->getLabel(), $error->getMessage());
  117. $event = $this->dispatchEvent('Error.beforeRender', ['error' => $error]);
  118. if ($event->isStopped()) {
  119. return true;
  120. }
  121. $renderer->write($renderer->render($error, $debug));
  122. } catch (Exception $e) {
  123. $logger->logMessage('error', 'Could not render error. Got: ' . $e->getMessage());
  124. return false;
  125. }
  126. return true;
  127. }
  128. /**
  129. * Get an instance of the renderer.
  130. *
  131. * @return \Cake\Error\ErrorRendererInterface
  132. */
  133. public function renderer(): ErrorRendererInterface
  134. {
  135. /** @var class-string<\Cake\Error\ErrorRendererInterface> $class */
  136. $class = $this->_getConfig('errorRenderer') ?: $this->chooseErrorRenderer();
  137. if (!in_array(ErrorRendererInterface::class, class_implements($class))) {
  138. throw new InvalidArgumentException(
  139. "Cannot use {$class} as an error renderer. It must implement \Cake\Error\ErrorRendererInterface."
  140. );
  141. }
  142. return new $class($this->_config);
  143. }
  144. /**
  145. * Get an instance of the logger.
  146. *
  147. * @return \Cake\Error\ErrorLoggerInterface
  148. */
  149. public function logger(): ErrorLoggerInterface
  150. {
  151. /** @var class-string<\Cake\Error\ErrorLoggerInterface> $class */
  152. $class = $this->_getConfig('logger', $this->_defaultConfig['logger']);
  153. if (!in_array(ErrorLoggerInterface::class, class_implements($class))) {
  154. throw new InvalidArgumentException(
  155. "Cannot use {$class} as an error logger. It must implement \Cake\Error\ErrorLoggerInterface."
  156. );
  157. }
  158. return new $class($this->_config);
  159. }
  160. }