1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\HttpKernel\EventListener;
13
14use Psr\Log\LoggerInterface;
15use Symfony\Component\Console\ConsoleEvents;
16use Symfony\Component\Console\Event\ConsoleEvent;
17use Symfony\Component\Console\Output\ConsoleOutputInterface;
18use Symfony\Component\ErrorHandler\ErrorHandler;
19use Symfony\Component\EventDispatcher\EventSubscriberInterface;
20use Symfony\Component\HttpKernel\Debug\FileLinkFormatter;
21use Symfony\Component\HttpKernel\Event\KernelEvent;
22use Symfony\Component\HttpKernel\KernelEvents;
23
24/**
25 * Configures errors and exceptions handlers.
26 *
27 * @author Nicolas Grekas <p@tchwork.com>
28 *
29 * @final
30 */
31class DebugHandlersListener implements EventSubscriberInterface
32{
33    private $earlyHandler;
34    private $exceptionHandler;
35    private $logger;
36    private $deprecationLogger;
37    private $levels;
38    private $throwAt;
39    private $scream;
40    private $fileLinkFormat;
41    private $scope;
42    private $firstCall = true;
43    private $hasTerminatedWithException;
44
45    /**
46     * @param callable|null                 $exceptionHandler A handler that must support \Throwable instances that will be called on Exception
47     * @param array|int                     $levels           An array map of E_* to LogLevel::* or an integer bit field of E_* constants
48     * @param int|null                      $throwAt          Thrown errors in a bit field of E_* constants, or null to keep the current value
49     * @param bool                          $scream           Enables/disables screaming mode, where even silenced errors are logged
50     * @param string|FileLinkFormatter|null $fileLinkFormat   The format for links to source files
51     * @param bool                          $scope            Enables/disables scoping mode
52     */
53    public function __construct(callable $exceptionHandler = null, LoggerInterface $logger = null, $levels = \E_ALL, ?int $throwAt = \E_ALL, bool $scream = true, $fileLinkFormat = null, bool $scope = true, LoggerInterface $deprecationLogger = null)
54    {
55        $handler = set_exception_handler('var_dump');
56        $this->earlyHandler = \is_array($handler) ? $handler[0] : null;
57        restore_exception_handler();
58
59        $this->exceptionHandler = $exceptionHandler;
60        $this->logger = $logger;
61        $this->levels = null === $levels ? \E_ALL : $levels;
62        $this->throwAt = \is_int($throwAt) ? $throwAt : (null === $throwAt ? null : ($throwAt ? \E_ALL : null));
63        $this->scream = $scream;
64        $this->fileLinkFormat = $fileLinkFormat;
65        $this->scope = $scope;
66        $this->deprecationLogger = $deprecationLogger;
67    }
68
69    /**
70     * Configures the error handler.
71     */
72    public function configure(object $event = null)
73    {
74        if ($event instanceof ConsoleEvent && !\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) {
75            return;
76        }
77        if (!$event instanceof KernelEvent ? !$this->firstCall : !$event->isMasterRequest()) {
78            return;
79        }
80        $this->firstCall = $this->hasTerminatedWithException = false;
81
82        $handler = set_exception_handler('var_dump');
83        $handler = \is_array($handler) ? $handler[0] : null;
84        restore_exception_handler();
85
86        if (!$handler instanceof ErrorHandler) {
87            $handler = $this->earlyHandler;
88        }
89
90        if ($handler instanceof ErrorHandler) {
91            if ($this->logger || $this->deprecationLogger) {
92                $this->setDefaultLoggers($handler);
93                if (\is_array($this->levels)) {
94                    $levels = 0;
95                    foreach ($this->levels as $type => $log) {
96                        $levels |= $type;
97                    }
98                } else {
99                    $levels = $this->levels;
100                }
101
102                if ($this->scream) {
103                    $handler->screamAt($levels);
104                }
105                if ($this->scope) {
106                    $handler->scopeAt($levels & ~\E_USER_DEPRECATED & ~\E_DEPRECATED);
107                } else {
108                    $handler->scopeAt(0, true);
109                }
110                $this->logger = $this->deprecationLogger = $this->levels = null;
111            }
112            if (null !== $this->throwAt) {
113                $handler->throwAt($this->throwAt, true);
114            }
115        }
116        if (!$this->exceptionHandler) {
117            if ($event instanceof KernelEvent) {
118                if (method_exists($kernel = $event->getKernel(), 'terminateWithException')) {
119                    $request = $event->getRequest();
120                    $hasRun = &$this->hasTerminatedWithException;
121                    $this->exceptionHandler = static function (\Throwable $e) use ($kernel, $request, &$hasRun) {
122                        if ($hasRun) {
123                            throw $e;
124                        }
125
126                        $hasRun = true;
127                        $kernel->terminateWithException($e, $request);
128                    };
129                }
130            } elseif ($event instanceof ConsoleEvent && $app = $event->getCommand()->getApplication()) {
131                $output = $event->getOutput();
132                if ($output instanceof ConsoleOutputInterface) {
133                    $output = $output->getErrorOutput();
134                }
135                $this->exceptionHandler = static function (\Throwable $e) use ($app, $output) {
136                    $app->renderThrowable($e, $output);
137                };
138            }
139        }
140        if ($this->exceptionHandler) {
141            if ($handler instanceof ErrorHandler) {
142                $handler->setExceptionHandler($this->exceptionHandler);
143            }
144            $this->exceptionHandler = null;
145        }
146    }
147
148    private function setDefaultLoggers(ErrorHandler $handler): void
149    {
150        if (\is_array($this->levels)) {
151            $levelsDeprecatedOnly = [];
152            $levelsWithoutDeprecated = [];
153            foreach ($this->levels as $type => $log) {
154                if (\E_DEPRECATED == $type || \E_USER_DEPRECATED == $type) {
155                    $levelsDeprecatedOnly[$type] = $log;
156                } else {
157                    $levelsWithoutDeprecated[$type] = $log;
158                }
159            }
160        } else {
161            $levelsDeprecatedOnly = $this->levels & (\E_DEPRECATED | \E_USER_DEPRECATED);
162            $levelsWithoutDeprecated = $this->levels & ~\E_DEPRECATED & ~\E_USER_DEPRECATED;
163        }
164
165        $defaultLoggerLevels = $this->levels;
166        if ($this->deprecationLogger && $levelsDeprecatedOnly) {
167            $handler->setDefaultLogger($this->deprecationLogger, $levelsDeprecatedOnly);
168            $defaultLoggerLevels = $levelsWithoutDeprecated;
169        }
170
171        if ($this->logger && $defaultLoggerLevels) {
172            $handler->setDefaultLogger($this->logger, $defaultLoggerLevels);
173        }
174    }
175
176    public static function getSubscribedEvents(): array
177    {
178        $events = [KernelEvents::REQUEST => ['configure', 2048]];
179
180        if (\defined('Symfony\Component\Console\ConsoleEvents::COMMAND')) {
181            $events[ConsoleEvents::COMMAND] = ['configure', 2048];
182        }
183
184        return $events;
185    }
186}
187