Blame | Letzte Änderung | Log anzeigen | RSS feed
<?phpnamespace Illuminate\Foundation\Exceptions;use Closure;use Exception;use Illuminate\Auth\Access\AuthorizationException;use Illuminate\Auth\AuthenticationException;use Illuminate\Console\View\Components\BulletList;use Illuminate\Console\View\Components\Error;use Illuminate\Contracts\Container\Container;use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandlerContract;use Illuminate\Contracts\Foundation\ExceptionRenderer;use Illuminate\Contracts\Support\Responsable;use Illuminate\Database\Eloquent\ModelNotFoundException;use Illuminate\Database\MultipleRecordsFoundException;use Illuminate\Database\RecordsNotFoundException;use Illuminate\Http\Exceptions\HttpResponseException;use Illuminate\Http\JsonResponse;use Illuminate\Http\RedirectResponse;use Illuminate\Http\Response;use Illuminate\Routing\Exceptions\BackedEnumCaseNotFoundException;use Illuminate\Routing\Router;use Illuminate\Session\TokenMismatchException;use Illuminate\Support\Arr;use Illuminate\Support\Facades\Auth;use Illuminate\Support\Reflector;use Illuminate\Support\Traits\ReflectsClosures;use Illuminate\Support\ViewErrorBag;use Illuminate\Validation\ValidationException;use InvalidArgumentException;use Psr\Log\LoggerInterface;use Psr\Log\LogLevel;use Symfony\Component\Console\Application as ConsoleApplication;use Symfony\Component\Console\Exception\CommandNotFoundException;use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException;use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirectResponse;use Symfony\Component\HttpFoundation\Response as SymfonyResponse;use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;use Symfony\Component\HttpKernel\Exception\HttpException;use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;use Throwable;class Handler implements ExceptionHandlerContract{use ReflectsClosures;/*** The container implementation.** @var \Illuminate\Contracts\Container\Container*/protected $container;/*** A list of the exception types that are not reported.** @var array<int, class-string<\Throwable>>*/protected $dontReport = [];/*** The callbacks that should be used during reporting.** @var \Illuminate\Foundation\Exceptions\ReportableHandler[]*/protected $reportCallbacks = [];/*** A map of exceptions with their corresponding custom log levels.** @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>*/protected $levels = [];/*** The callbacks that should be used during rendering.** @var \Closure[]*/protected $renderCallbacks = [];/*** The registered exception mappings.** @var array<string, \Closure>*/protected $exceptionMap = [];/*** A list of the internal exception types that should not be reported.** @var array<int, class-string<\Throwable>>*/protected $internalDontReport = [AuthenticationException::class,AuthorizationException::class,BackedEnumCaseNotFoundException::class,HttpException::class,HttpResponseException::class,ModelNotFoundException::class,MultipleRecordsFoundException::class,RecordsNotFoundException::class,SuspiciousOperationException::class,TokenMismatchException::class,ValidationException::class,];/*** A list of the inputs that are never flashed for validation exceptions.** @var array<int, string>*/protected $dontFlash = ['current_password','password','password_confirmation',];/*** Create a new exception handler instance.** @param \Illuminate\Contracts\Container\Container $container* @return void*/public function __construct(Container $container){$this->container = $container;$this->register();}/*** Register the exception handling callbacks for the application.** @return void*/public function register(){//}/*** Register a reportable callback.** @param callable $reportUsing* @return \Illuminate\Foundation\Exceptions\ReportableHandler*/public function reportable(callable $reportUsing){if (! $reportUsing instanceof Closure) {$reportUsing = Closure::fromCallable($reportUsing);}return tap(new ReportableHandler($reportUsing), function ($callback) {$this->reportCallbacks[] = $callback;});}/*** Register a renderable callback.** @param callable $renderUsing* @return $this*/public function renderable(callable $renderUsing){if (! $renderUsing instanceof Closure) {$renderUsing = Closure::fromCallable($renderUsing);}$this->renderCallbacks[] = $renderUsing;return $this;}/*** Register a new exception mapping.** @param \Closure|string $from* @param \Closure|string|null $to* @return $this** @throws \InvalidArgumentException*/public function map($from, $to = null){if (is_string($to)) {$to = fn ($exception) => new $to('', 0, $exception);}if (is_callable($from) && is_null($to)) {$from = $this->firstClosureParameterType($to = $from);}if (! is_string($from) || ! $to instanceof Closure) {throw new InvalidArgumentException('Invalid exception mapping.');}$this->exceptionMap[$from] = $to;return $this;}/*** Indicate that the given exception type should not be reported.** @param string $class* @return $this*/public function ignore(string $class){$this->dontReport[] = $class;return $this;}/*** Set the log level for the given exception type.** @param class-string<\Throwable> $type* @param \Psr\Log\LogLevel::* $level* @return $this*/public function level($type, $level){$this->levels[$type] = $level;return $this;}/*** Report or log an exception.** @param \Throwable $e* @return void** @throws \Throwable*/public function report(Throwable $e){$e = $this->mapException($e);if ($this->shouldntReport($e)) {return;}if (Reflector::isCallable($reportCallable = [$e, 'report']) &&$this->container->call($reportCallable) !== false) {return;}foreach ($this->reportCallbacks as $reportCallback) {if ($reportCallback->handles($e) && $reportCallback($e) === false) {return;}}try {$logger = $this->container->make(LoggerInterface::class);} catch (Exception $ex) {throw $e;}$level = Arr::first($this->levels, fn ($level, $type) => $e instanceof $type, LogLevel::ERROR);$context = $this->buildExceptionContext($e);method_exists($logger, $level)? $logger->{$level}($e->getMessage(), $context): $logger->log($level, $e->getMessage(), $context);}/*** Determine if the exception should be reported.** @param \Throwable $e* @return bool*/public function shouldReport(Throwable $e){return ! $this->shouldntReport($e);}/*** Determine if the exception is in the "do not report" list.** @param \Throwable $e* @return bool*/protected function shouldntReport(Throwable $e){$dontReport = array_merge($this->dontReport, $this->internalDontReport);return ! is_null(Arr::first($dontReport, fn ($type) => $e instanceof $type));}/*** Create the context array for logging the given exception.** @param \Throwable $e* @return array*/protected function buildExceptionContext(Throwable $e){return array_merge($this->exceptionContext($e),$this->context(),['exception' => $e]);}/*** Get the default exception context variables for logging.** @param \Throwable $e* @return array*/protected function exceptionContext(Throwable $e){if (method_exists($e, 'context')) {return $e->context();}return [];}/*** Get the default context variables for logging.** @return array*/protected function context(){try {return array_filter(['userId' => Auth::id(),]);} catch (Throwable $e) {return [];}}/*** Render an exception into an HTTP response.** @param \Illuminate\Http\Request $request* @param \Throwable $e* @return \Symfony\Component\HttpFoundation\Response** @throws \Throwable*/public function render($request, Throwable $e){if (method_exists($e, 'render') && $response = $e->render($request)) {return Router::toResponse($request, $response);}if ($e instanceof Responsable) {return $e->toResponse($request);}$e = $this->prepareException($this->mapException($e));if ($response = $this->renderViaCallbacks($request, $e)) {return $response;}return match (true) {$e instanceof HttpResponseException => $e->getResponse(),$e instanceof AuthenticationException => $this->unauthenticated($request, $e),$e instanceof ValidationException => $this->convertValidationExceptionToResponse($e, $request),default => $this->renderExceptionResponse($request, $e),};}/*** Prepare exception for rendering.** @param \Throwable $e* @return \Throwable*/protected function prepareException(Throwable $e){return match (true) {$e instanceof BackedEnumCaseNotFoundException => new NotFoundHttpException($e->getMessage(), $e),$e instanceof ModelNotFoundException => new NotFoundHttpException($e->getMessage(), $e),$e instanceof AuthorizationException && $e->hasStatus() => new HttpException($e->status(), $e->response()?->message() ?: (Response::$statusTexts[$e->status()] ?? 'Whoops, looks like something went wrong.'), $e),$e instanceof AuthorizationException && ! $e->hasStatus() => new AccessDeniedHttpException($e->getMessage(), $e),$e instanceof TokenMismatchException => new HttpException(419, $e->getMessage(), $e),$e instanceof SuspiciousOperationException => new NotFoundHttpException('Bad hostname provided.', $e),$e instanceof RecordsNotFoundException => new NotFoundHttpException('Not found.', $e),default => $e,};}/*** Map the exception using a registered mapper if possible.** @param \Throwable $e* @return \Throwable*/protected function mapException(Throwable $e){if (method_exists($e, 'getInnerException') &&($inner = $e->getInnerException()) instanceof Throwable) {return $inner;}foreach ($this->exceptionMap as $class => $mapper) {if (is_a($e, $class)) {return $mapper($e);}}return $e;}/*** Try to render a response from request and exception via render callbacks.** @param \Illuminate\Http\Request $request* @param \Throwable $e* @return mixed** @throws \ReflectionException*/protected function renderViaCallbacks($request, Throwable $e){foreach ($this->renderCallbacks as $renderCallback) {foreach ($this->firstClosureParameterTypes($renderCallback) as $type) {if (is_a($e, $type)) {$response = $renderCallback($e, $request);if (! is_null($response)) {return $response;}}}}}/*** Render a default exception response if any.** @param \Illuminate\Http\Request $request* @param \Throwable $e* @return \Symfony\Component\HttpFoundation\Response*/protected function renderExceptionResponse($request, Throwable $e){return $this->shouldReturnJson($request, $e)? $this->prepareJsonResponse($request, $e): $this->prepareResponse($request, $e);}/*** Convert an authentication exception into a response.** @param \Illuminate\Http\Request $request* @param \Illuminate\Auth\AuthenticationException $exception* @return \Symfony\Component\HttpFoundation\Response*/protected function unauthenticated($request, AuthenticationException $exception){return $this->shouldReturnJson($request, $exception)? response()->json(['message' => $exception->getMessage()], 401): redirect()->guest($exception->redirectTo() ?? route('login'));}/*** Create a response object from the given validation exception.** @param \Illuminate\Validation\ValidationException $e* @param \Illuminate\Http\Request $request* @return \Symfony\Component\HttpFoundation\Response*/protected function convertValidationExceptionToResponse(ValidationException $e, $request){if ($e->response) {return $e->response;}return $this->shouldReturnJson($request, $e)? $this->invalidJson($request, $e): $this->invalid($request, $e);}/*** Convert a validation exception into a response.** @param \Illuminate\Http\Request $request* @param \Illuminate\Validation\ValidationException $exception* @return \Illuminate\Http\Response*/protected function invalid($request, ValidationException $exception){return redirect($exception->redirectTo ?? url()->previous())->withInput(Arr::except($request->input(), $this->dontFlash))->withErrors($exception->errors(), $request->input('_error_bag', $exception->errorBag));}/*** Convert a validation exception into a JSON response.** @param \Illuminate\Http\Request $request* @param \Illuminate\Validation\ValidationException $exception* @return \Illuminate\Http\JsonResponse*/protected function invalidJson($request, ValidationException $exception){return response()->json(['message' => $exception->getMessage(),'errors' => $exception->errors(),], $exception->status);}/*** Determine if the exception handler response should be JSON.** @param \Illuminate\Http\Request $request* @param \Throwable $e* @return bool*/protected function shouldReturnJson($request, Throwable $e){return $request->expectsJson();}/*** Prepare a response for the given exception.** @param \Illuminate\Http\Request $request* @param \Throwable $e* @return \Symfony\Component\HttpFoundation\Response*/protected function prepareResponse($request, Throwable $e){if (! $this->isHttpException($e) && config('app.debug')) {return $this->toIlluminateResponse($this->convertExceptionToResponse($e), $e)->prepare($request);}if (! $this->isHttpException($e)) {$e = new HttpException(500, $e->getMessage(), $e);}return $this->toIlluminateResponse($this->renderHttpException($e), $e)->prepare($request);}/*** Create a Symfony response for the given exception.** @param \Throwable $e* @return \Symfony\Component\HttpFoundation\Response*/protected function convertExceptionToResponse(Throwable $e){return new SymfonyResponse($this->renderExceptionContent($e),$this->isHttpException($e) ? $e->getStatusCode() : 500,$this->isHttpException($e) ? $e->getHeaders() : []);}/*** Get the response content for the given exception.** @param \Throwable $e* @return string*/protected function renderExceptionContent(Throwable $e){try {return config('app.debug') && app()->has(ExceptionRenderer::class)? $this->renderExceptionWithCustomRenderer($e): $this->renderExceptionWithSymfony($e, config('app.debug'));} catch (Throwable $e) {return $this->renderExceptionWithSymfony($e, config('app.debug'));}}/*** Render an exception to a string using the registered `ExceptionRenderer`.** @param \Throwable $e* @return string*/protected function renderExceptionWithCustomRenderer(Throwable $e){return app(ExceptionRenderer::class)->render($e);}/*** Render an exception to a string using Symfony.** @param \Throwable $e* @param bool $debug* @return string*/protected function renderExceptionWithSymfony(Throwable $e, $debug){$renderer = new HtmlErrorRenderer($debug);return $renderer->render($e)->getAsString();}/*** Render the given HttpException.** @param \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e* @return \Symfony\Component\HttpFoundation\Response*/protected function renderHttpException(HttpExceptionInterface $e){$this->registerErrorViewPaths();if ($view = $this->getHttpExceptionView($e)) {return response()->view($view, ['errors' => new ViewErrorBag,'exception' => $e,], $e->getStatusCode(), $e->getHeaders());}return $this->convertExceptionToResponse($e);}/*** Register the error template hint paths.** @return void*/protected function registerErrorViewPaths(){(new RegisterErrorViewPaths)();}/*** Get the view used to render HTTP exceptions.** @param \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e* @return string|null*/protected function getHttpExceptionView(HttpExceptionInterface $e){$view = 'errors::'.$e->getStatusCode();if (view()->exists($view)) {return $view;}$view = substr($view, 0, -2).'xx';if (view()->exists($view)) {return $view;}return null;}/*** Map the given exception into an Illuminate response.** @param \Symfony\Component\HttpFoundation\Response $response* @param \Throwable $e* @return \Illuminate\Http\Response*/protected function toIlluminateResponse($response, Throwable $e){if ($response instanceof SymfonyRedirectResponse) {$response = new RedirectResponse($response->getTargetUrl(), $response->getStatusCode(), $response->headers->all());} else {$response = new Response($response->getContent(), $response->getStatusCode(), $response->headers->all());}return $response->withException($e);}/*** Prepare a JSON response for the given exception.** @param \Illuminate\Http\Request $request* @param \Throwable $e* @return \Illuminate\Http\JsonResponse*/protected function prepareJsonResponse($request, Throwable $e){return new JsonResponse($this->convertExceptionToArray($e),$this->isHttpException($e) ? $e->getStatusCode() : 500,$this->isHttpException($e) ? $e->getHeaders() : [],JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);}/*** Convert the given exception to an array.** @param \Throwable $e* @return array*/protected function convertExceptionToArray(Throwable $e){return config('app.debug') ? ['message' => $e->getMessage(),'exception' => get_class($e),'file' => $e->getFile(),'line' => $e->getLine(),'trace' => collect($e->getTrace())->map(fn ($trace) => Arr::except($trace, ['args']))->all(),] : ['message' => $this->isHttpException($e) ? $e->getMessage() : 'Server Error',];}/*** Render an exception to the console.** @param \Symfony\Component\Console\Output\OutputInterface $output* @param \Throwable $e* @return void** @internal This method is not meant to be used or overwritten outside the framework.*/public function renderForConsole($output, Throwable $e){if ($e instanceof CommandNotFoundException) {$message = str($e->getMessage())->explode('.')->first();if (! empty($alternatives = $e->getAlternatives())) {$message .= '. Did you mean one of these?';with(new Error($output))->render($message);with(new BulletList($output))->render($e->getAlternatives());$output->writeln('');} else {with(new Error($output))->render($message);}return;}(new ConsoleApplication)->renderThrowable($e, $output);}/*** Determine if the given exception is an HTTP exception.** @param \Throwable $e* @return bool*/protected function isHttpException(Throwable $e){return $e instanceof HttpExceptionInterface;}}