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\Debug\ErrorHandler;
19use Symfony\Component\Debug\ExceptionHandler;
20use Symfony\Component\EventDispatcher\Event;
21use Symfony\Component\EventDispatcher\EventSubscriberInterface;
22use Symfony\Component\HttpKernel\Debug\FileLinkFormatter;
23use Symfony\Component\HttpKernel\Event\KernelEvent;
24use Symfony\Component\HttpKernel\KernelEvents;
25
26/**
27 * Configures errors and exceptions handlers.
28 *
29 * @author Nicolas Grekas <p@tchwork.com>
30 */
31class DebugHandlersListener implements EventSubscriberInterface
32{
33    private $exceptionHandler;
34    private $logger;
35    private $levels;
36    private $throwAt;
37    private $scream;
38    private $fileLinkFormat;
39    private $scope;
40    private $firstCall = true;
41    private $hasTerminatedWithException;
42
43    /**
44     * @param callable|null                 $exceptionHandler A handler that will be called on Exception
45     * @param LoggerInterface|null          $logger           A PSR-3 logger
46     * @param array|int                     $levels           An array map of E_* to LogLevel::* or an integer bit field of E_* constants
47     * @param int|null                      $throwAt          Thrown errors in a bit field of E_* constants, or null to keep the current value
48     * @param bool                          $scream           Enables/disables screaming mode, where even silenced errors are logged
49     * @param string|FileLinkFormatter|null $fileLinkFormat   The format for links to source files
50     * @param bool                          $scope            Enables/disables scoping mode
51     */
52    public function __construct(callable $exceptionHandler = null, LoggerInterface $logger = null, $levels = E_ALL, $throwAt = E_ALL, $scream = true, $fileLinkFormat = null, $scope = true)
53    {
54        $this->exceptionHandler = $exceptionHandler;
55        $this->logger = $logger;
56        $this->levels = null === $levels ? E_ALL : $levels;
57        $this->throwAt = is_numeric($throwAt) ? (int) $throwAt : (null === $throwAt ? null : ($throwAt ? E_ALL : null));
58        $this->scream = (bool) $scream;
59        $this->fileLinkFormat = $fileLinkFormat;
60        $this->scope = (bool) $scope;
61    }
62
63    /**
64     * Configures the error handler.
65     */
66    public function configure(Event $event = null)
67    {
68        if ($event instanceof ConsoleEvent && !\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) {
69            return;
70        }
71        if (!$event instanceof KernelEvent ? !$this->firstCall : !$event->isMasterRequest()) {
72            return;
73        }
74        $this->firstCall = $this->hasTerminatedWithException = false;
75
76        $handler = set_exception_handler('var_dump');
77        $handler = \is_array($handler) ? $handler[0] : null;
78        restore_exception_handler();
79
80        if ($this->logger || null !== $this->throwAt) {
81            if ($handler instanceof ErrorHandler) {
82                if ($this->logger) {
83                    $handler->setDefaultLogger($this->logger, $this->levels);
84                    if (\is_array($this->levels)) {
85                        $levels = 0;
86                        foreach ($this->levels as $type => $log) {
87                            $levels |= $type;
88                        }
89                    } else {
90                        $levels = $this->levels;
91                    }
92                    if ($this->scream) {
93                        $handler->screamAt($levels);
94                    }
95                    if ($this->scope) {
96                        $handler->scopeAt($levels & ~E_USER_DEPRECATED & ~E_DEPRECATED);
97                    } else {
98                        $handler->scopeAt(0, true);
99                    }
100                    $this->logger = $this->levels = null;
101                }
102                if (null !== $this->throwAt) {
103                    $handler->throwAt($this->throwAt, true);
104                }
105            }
106        }
107        if (!$this->exceptionHandler) {
108            if ($event instanceof KernelEvent) {
109                if (method_exists($kernel = $event->getKernel(), 'terminateWithException')) {
110                    $request = $event->getRequest();
111                    $hasRun = &$this->hasTerminatedWithException;
112                    $this->exceptionHandler = static function (\Exception $e) use ($kernel, $request, &$hasRun) {
113                        if ($hasRun) {
114                            throw $e;
115                        }
116                        $hasRun = true;
117                        $kernel->terminateWithException($e, $request);
118                    };
119                }
120            } elseif ($event instanceof ConsoleEvent && $app = $event->getCommand()->getApplication()) {
121                $output = $event->getOutput();
122                if ($output instanceof ConsoleOutputInterface) {
123                    $output = $output->getErrorOutput();
124                }
125                $this->exceptionHandler = function ($e) use ($app, $output) {
126                    $app->renderException($e, $output);
127                };
128            }
129        }
130        if ($this->exceptionHandler) {
131            if ($handler instanceof ErrorHandler) {
132                $h = $handler->setExceptionHandler('var_dump');
133                if (\is_array($h) && $h[0] instanceof ExceptionHandler) {
134                    $handler->setExceptionHandler($h);
135                    $handler = $h[0];
136                } else {
137                    $handler->setExceptionHandler($this->exceptionHandler);
138                }
139            }
140            if ($handler instanceof ExceptionHandler) {
141                $handler->setHandler($this->exceptionHandler);
142                if (null !== $this->fileLinkFormat) {
143                    $handler->setFileLinkFormat($this->fileLinkFormat);
144                }
145            }
146            $this->exceptionHandler = null;
147        }
148    }
149
150    public static function getSubscribedEvents()
151    {
152        $events = [KernelEvents::REQUEST => ['configure', 2048]];
153
154        if (\defined('Symfony\Component\Console\ConsoleEvents::COMMAND')) {
155            $events[ConsoleEvents::COMMAND] = ['configure', 2048];
156        }
157
158        return $events;
159    }
160}
161