Revision 148 | Blame | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed
<?php/*** Whoops - php errors for cool kids* @author Filipe Dobreira <http://github.com/filp>*/namespace Whoops;use InvalidArgumentException;use Throwable;use Whoops\Exception\ErrorException;use Whoops\Handler\CallbackHandler;use Whoops\Handler\Handler;use Whoops\Handler\HandlerInterface;use Whoops\Inspector\CallableInspectorFactory;use Whoops\Inspector\InspectorFactory;use Whoops\Inspector\InspectorFactoryInterface;use Whoops\Inspector\InspectorInterface;use Whoops\Util\Misc;use Whoops\Util\SystemFacade;final class Run implements RunInterface{/*** @var bool*/private $isRegistered;/*** @var bool*/private $allowQuit = true;/*** @var bool*/private $sendOutput = true;/*** @var integer|false*/private $sendHttpCode = 500;/*** @var integer|false*/private $sendExitCode = 1;/*** @var HandlerInterface[]*/private $handlerStack = [];/*** @var array* @psalm-var list<array{patterns: string, levels: int}>*/private $silencedPatterns = [];/*** @var SystemFacade*/private $system;/*** In certain scenarios, like in shutdown handler, we can not throw exceptions.** @var bool*/private $canThrowExceptions = true;/*** The inspector factory to create inspectors.** @var InspectorFactoryInterface*/private $inspectorFactory;/*** @var array<callable>*/private $frameFilters = [];public function __construct(SystemFacade $system = null){$this->system = $system ?: new SystemFacade;$this->inspectorFactory = new InspectorFactory();}/*** Explicitly request your handler runs as the last of all currently registered handlers.** @param callable|HandlerInterface $handler** @return Run*/public function appendHandler($handler){array_unshift($this->handlerStack, $this->resolveHandler($handler));return $this;}/*** Explicitly request your handler runs as the first of all currently registered handlers.** @param callable|HandlerInterface $handler** @return Run*/public function prependHandler($handler){return $this->pushHandler($handler);}/*** Register your handler as the last of all currently registered handlers (to be executed first).* Prefer using appendHandler and prependHandler for clarity.** @param callable|HandlerInterface $handler** @return Run** @throws InvalidArgumentException If argument is not callable or instance of HandlerInterface.*/public function pushHandler($handler){$this->handlerStack[] = $this->resolveHandler($handler);return $this;}/*** Removes and returns the last handler pushed to the handler stack.** @see Run::removeFirstHandler(), Run::removeLastHandler()** @return HandlerInterface|null*/public function popHandler(){return array_pop($this->handlerStack);}/*** Removes the first handler.** @return void*/public function removeFirstHandler(){array_pop($this->handlerStack);}/*** Removes the last handler.** @return void*/public function removeLastHandler(){array_shift($this->handlerStack);}/*** Returns an array with all handlers, in the order they were added to the stack.** @return array*/public function getHandlers(){return $this->handlerStack;}/*** Clears all handlers in the handlerStack, including the default PrettyPage handler.** @return Run*/public function clearHandlers(){$this->handlerStack = [];return $this;}public function getFrameFilters(){return $this->frameFilters;}public function clearFrameFilters(){$this->frameFilters = [];return $this;}/*** Registers this instance as an error handler.** @return Run*/public function register(){if (!$this->isRegistered) {// Workaround PHP bug 42098// https://bugs.php.net/bug.php?id=42098class_exists("\\Whoops\\Exception\\ErrorException");class_exists("\\Whoops\\Exception\\FrameCollection");class_exists("\\Whoops\\Exception\\Frame");class_exists("\\Whoops\\Exception\\Inspector");class_exists("\\Whoops\\Inspector\\InspectorFactory");$this->system->setErrorHandler([$this, self::ERROR_HANDLER]);$this->system->setExceptionHandler([$this, self::EXCEPTION_HANDLER]);$this->system->registerShutdownFunction([$this, self::SHUTDOWN_HANDLER]);$this->isRegistered = true;}return $this;}/*** Unregisters all handlers registered by this Whoops\Run instance.** @return Run*/public function unregister(){if ($this->isRegistered) {$this->system->restoreExceptionHandler();$this->system->restoreErrorHandler();$this->isRegistered = false;}return $this;}/*** Should Whoops allow Handlers to force the script to quit?** @param bool|int $exit** @return bool*/public function allowQuit($exit = null){if (func_num_args() == 0) {return $this->allowQuit;}return $this->allowQuit = (bool) $exit;}/*** Silence particular errors in particular files.** @param array|string $patterns List or a single regex pattern to match.* @param int $levels Defaults to E_STRICT | E_DEPRECATED.** @return Run*/public function silenceErrorsInPaths($patterns, $levels = 10240){$this->silencedPatterns = array_merge($this->silencedPatterns,array_map(function ($pattern) use ($levels) {return ["pattern" => $pattern,"levels" => $levels,];},(array) $patterns));return $this;}/*** Returns an array with silent errors in path configuration.** @return array*/public function getSilenceErrorsInPaths(){return $this->silencedPatterns;}/*** Should Whoops send HTTP error code to the browser if possible?* Whoops will by default send HTTP code 500, but you may wish to* use 502, 503, or another 5xx family code.** @param bool|int $code** @return int|false** @throws InvalidArgumentException*/public function sendHttpCode($code = null){if (func_num_args() == 0) {return $this->sendHttpCode;}if (!$code) {return $this->sendHttpCode = false;}if ($code === true) {$code = 500;}if ($code < 400 || 600 <= $code) {throw new InvalidArgumentException("Invalid status code '$code', must be 4xx or 5xx");}return $this->sendHttpCode = $code;}/*** Should Whoops exit with a specific code on the CLI if possible?* Whoops will exit with 1 by default, but you can specify something else.** @param int $code** @return int** @throws InvalidArgumentException*/public function sendExitCode($code = null){if (func_num_args() == 0) {return $this->sendExitCode;}if ($code < 0 || 255 <= $code) {throw new InvalidArgumentException("Invalid status code '$code', must be between 0 and 254");}return $this->sendExitCode = (int) $code;}/*** Should Whoops push output directly to the client?* If this is false, output will be returned by handleException.** @param bool|int $send** @return bool*/public function writeToOutput($send = null){if (func_num_args() == 0) {return $this->sendOutput;}return $this->sendOutput = (bool) $send;}/*** Handles an exception, ultimately generating a Whoops error page.** @param Throwable $exception** @return string Output generated by handlers.*/public function handleException($exception){// Walk the registered handlers in the reverse order// they were registered, and pass off the exception$inspector = $this->getInspector($exception);// Capture output produced while handling the exception,// we might want to send it straight away to the client,// or return it silently.$this->system->startOutputBuffering();// Just in case there are no handlers:$handlerResponse = null;$handlerContentType = null;try {foreach (array_reverse($this->handlerStack) as $handler) {$handler->setRun($this);$handler->setInspector($inspector);$handler->setException($exception);// The HandlerInterface does not require an Exception passed to handle()// and neither of our bundled handlers use it.// However, 3rd party handlers may have already relied on this parameter,// and removing it would be possibly breaking for users.$handlerResponse = $handler->handle($exception);// Collect the content type for possible sending in the headers.$handlerContentType = method_exists($handler, 'contentType') ? $handler->contentType() : null;if (in_array($handlerResponse, [Handler::LAST_HANDLER, Handler::QUIT])) {// The Handler has handled the exception in some way, and// wishes to quit execution (Handler::QUIT), or skip any// other handlers (Handler::LAST_HANDLER). If $this->allowQuit// is false, Handler::QUIT behaves like Handler::LAST_HANDLERbreak;}}$willQuit = $handlerResponse == Handler::QUIT && $this->allowQuit();} finally {$output = $this->system->cleanOutputBuffer();}// If we're allowed to, send output generated by handlers directly// to the output, otherwise, and if the script doesn't quit, return// it so that it may be used by the callerif ($this->writeToOutput()) {// @todo Might be able to clean this up a bit betterif ($willQuit) {// Cleanup all other output buffers before sending our output:while ($this->system->getOutputBufferLevel() > 0) {$this->system->endOutputBuffering();}// Send any headers if needed:if (Misc::canSendHeaders() && $handlerContentType) {header("Content-Type: {$handlerContentType}");}}$this->writeToOutputNow($output);}if ($willQuit) {// HHVM fix for https://github.com/facebook/hhvm/issues/4055$this->system->flushOutputBuffer();$this->system->stopExecution($this->sendExitCode());}return $output;}/*** Converts generic PHP errors to \ErrorException instances, before passing them off to be handled.** This method MUST be compatible with set_error_handler.** @param int $level* @param string $message* @param string|null $file* @param int|null $line** @return bool** @throws ErrorException*/public function handleError($level, $message, $file = null, $line = null){if ($level & $this->system->getErrorReportingLevel()) {foreach ($this->silencedPatterns as $entry) {$pathMatches = (bool) preg_match($entry["pattern"], $file);$levelMatches = $level & $entry["levels"];if ($pathMatches && $levelMatches) {// Ignore the error, abort handling// See https://github.com/filp/whoops/issues/418return true;}}// XXX we pass $level for the "code" param only for BC reasons.// see https://github.com/filp/whoops/issues/267$exception = new ErrorException($message, /*code*/ $level, /*severity*/ $level, $file, $line);if ($this->canThrowExceptions) {throw $exception;} else {$this->handleException($exception);}// Do not propagate errors which were already handled by Whoops.return true;}// Propagate error to the next handler, allows error_get_last() to// work on silenced errors.return false;}/*** Special case to deal with Fatal errors and the like.** @return void*/public function handleShutdown(){// If we reached this step, we are in shutdown handler.// An exception thrown in a shutdown handler will not be propagated// to the exception handler. Pass that information along.$this->canThrowExceptions = false;$error = $this->system->getLastError();if ($error && Misc::isLevelFatal($error['type'])) {// If there was a fatal error,// it was not handled in handleError yet.$this->allowQuit = false;$this->handleError($error['type'],$error['message'],$error['file'],$error['line']);}}/*** @param InspectorFactoryInterface $factory** @return void*/public function setInspectorFactory(InspectorFactoryInterface $factory){$this->inspectorFactory = $factory;}public function addFrameFilter($filterCallback){if (!is_callable($filterCallback)) {throw new \InvalidArgumentException(sprintf("A frame filter must be of type callable, %s type given.",gettype($filterCallback)));}$this->frameFilters[] = $filterCallback;return $this;}/*** @param Throwable $exception** @return InspectorInterface*/private function getInspector($exception){return $this->inspectorFactory->create($exception);}/*** Resolves the giving handler.** @param callable|HandlerInterface $handler** @return HandlerInterface** @throws InvalidArgumentException*/private function resolveHandler($handler){if (is_callable($handler)) {$handler = new CallbackHandler($handler);}if (!$handler instanceof HandlerInterface) {throw new InvalidArgumentException("Handler must be a callable, or instance of ". "Whoops\\Handler\\HandlerInterface");}return $handler;}/*** Echo something to the browser.** @param string $output** @return Run*/private function writeToOutputNow($output){if ($this->sendHttpCode() && Misc::canSendHeaders()) {$this->system->setHttpResponseCode($this->sendHttpCode());}echo $output;return $this;}}