Completed
Push — master ( ce3f18...1319dc )
by Christian
05:54
created

ExceptionController::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 15
ccs 8
cts 8
cp 1
rs 9.4285
cc 1
eloc 13
nc 1
nop 6
crap 1
1
<?php
2
3
/*
4
 * This file is part of the FOSRestBundle package.
5
 *
6
 * (c) FriendsOfSymfony <http://friendsofsymfony.github.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
12
namespace FOS\RestBundle\Controller;
13
14
use FOS\RestBundle\Negotiation\FormatNegotiator;
15
use FOS\RestBundle\Util\StopFormatListenerException;
16
use FOS\RestBundle\Util\ExceptionWrapper;
17
use FOS\RestBundle\View\ExceptionWrapperHandlerInterface;
18
use FOS\RestBundle\View\View;
19
use FOS\RestBundle\View\ViewHandlerInterface;
20
use Symfony\Component\HttpFoundation\Request;
21
use Symfony\Component\HttpFoundation\Response;
22
use Symfony\Component\Debug\Exception\FlattenException;
23
use Symfony\Component\HttpKernel\Log\DebugLoggerInterface;
24
25
/**
26
 * Custom ExceptionController that uses the view layer and supports HTTP response status code mapping.
27
 */
28
class ExceptionController
29
{
30
    private $exceptionWrapperHandler;
31
    private $formatNegotiator;
32
    private $viewHandler;
33
    private $exceptionCodes;
34
    private $exceptionMessages;
35
    private $showException;
36
37 4
    public function __construct(
38
        ExceptionWrapperHandlerInterface $exceptionWrapperHandler,
39
        FormatNegotiator $formatNegotiator,
40
        ViewHandlerInterface $viewHandler,
41
        array $exceptionCodes,
42
        array $exceptionMessages,
43
        $showException
44
    ) {
45 4
        $this->exceptionWrapperHandler = $exceptionWrapperHandler;
46 4
        $this->formatNegotiator = $formatNegotiator;
47 4
        $this->viewHandler = $viewHandler;
48 4
        $this->exceptionCodes = $exceptionCodes;
49 4
        $this->exceptionMessages = $exceptionMessages;
50 4
        $this->showException = $showException;
51 4
    }
52
53
    /**
54
     * @return ViewHandlerInterface
55
     */
56
    protected function getViewHandler()
57
    {
58
        return $this->viewHandler;
59
    }
60
61
    /**
62
     * Converts an Exception to a Response.
63
     *
64
     * @param Request              $request
65
     * @param FlattenException     $exception
66
     * @param DebugLoggerInterface $logger
67
     *
68
     * @throws \InvalidArgumentException
69
     *
70
     * @return Response
71
     */
72 4
    public function showAction(Request $request, FlattenException $exception, DebugLoggerInterface $logger = null)
73
    {
74
        try {
75 4
            $format = $this->getFormat($request, $request->getRequestFormat());
76 4
        } catch (\Exception $e) {
77
            $format = null;
78
        }
79 4
        if (null === $format) {
80
            $message = 'No matching accepted Response format could be determined, while handling: ';
81
            $message .= $this->getExceptionMessage($exception);
82
83
            return $this->createPlainResponse($message, Response::HTTP_NOT_ACCEPTABLE, $exception->getHeaders());
84
        }
85
86 4
        $currentContent = $this->getAndCleanOutputBuffering(
87 4
            $request->headers->get('X-Php-Ob-Level', -1)
88 4
        );
89 4
        $code = $this->getStatusCode($exception);
90 4
        $parameters = $this->getParameters($currentContent, $code, $exception, $logger, $format);
91 4
        $showException = $request->attributes->get('showException', $this->showException);
92
93
        try {
94 4
            $view = $this->createView($format, $exception, $code, $parameters, $request, $showException);
95
96 4
            $response = $this->viewHandler->handle($view);
97 4
        } catch (\Exception $e) {
98
            $message = 'An Exception was thrown while handling: ';
99
            $message .= $this->getExceptionMessage($exception);
100
            $response = $this->createPlainResponse($message, Response::HTTP_INTERNAL_SERVER_ERROR, $exception->getHeaders());
101
        }
102
103 4
        return $response;
104
    }
105
106
    /**
107
     * Returns a Response Object with content type text/plain.
108
     *
109
     * @param string $content
110
     * @param int    $status
111
     * @param array  $headers
112
     *
113
     * @return Response
114
     */
115
    private function createPlainResponse($content, $status, $headers)
116
    {
117
        $headers['content-type'] = 'text/plain';
118
119
        return new Response($content, $status, $headers);
120
    }
121
122
    /**
123
     * Creates a new ExceptionWrapper instance that can be overwritten by a custom
124
     * ExceptionController class.
125
     *
126
     * @param array $parameters output data
127
     *
128
     * @return ExceptionWrapper ExceptionWrapper instance
129
     */
130 4
    protected function createExceptionWrapper(array $parameters)
131
    {
132 4
        return $this->exceptionWrapperHandler->wrap($parameters);
133
    }
134
135
    /**
136
     * @param string           $format
137
     * @param FlattenException $exception
138
     * @param int              $code
139
     * @param array            $parameters
140
     * @param Request          $request
141
     * @param bool             $showException
142
     *
143
     * @return View
144
     */
145 4
    protected function createView($format, FlattenException $exception, $code, $parameters, Request $request, $showException)
146
    {
147 4
        $parameters = $this->createExceptionWrapper($parameters);
148 4
        $view = View::create($parameters, $code, $exception->getHeaders());
149 4
        $view->setFormat($format);
150
151 4
        return $view;
152
    }
153
154
    /**
155
     * Gets and cleans any content that was already outputted.
156
     *
157
     * This code comes from Symfony and should be synchronized on a regular basis
158
     * see src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php
159
     *
160
     * @return string
161
     */
162 4
    private function getAndCleanOutputBuffering($startObLevel)
163
    {
164 4
        if (ob_get_level() <= $startObLevel) {
165 4
            return '';
166
        }
167
        Response::closeOutputBuffers($startObLevel + 1, true);
168
169
        return ob_get_clean();
170
    }
171
172
    /**
173
     * Extracts the exception message.
174
     *
175
     * @param FlattenException $exception
176
     * @param array            $exceptionMap
177
     *
178
     * @return int|false
179
     */
180 4
    private function isSubclassOf($exception, $exceptionMap)
181
    {
182 4
        $exceptionClass = $exception->getClass();
183 4
        foreach ($exceptionMap as $exceptionMapClass => $value) {
184
            if ($value
185 4
                && ($exceptionClass === $exceptionMapClass || is_subclass_of($exceptionClass, $exceptionMapClass))
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if $exceptionMapClass can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
186 4
            ) {
187 4
                return $value;
188
            }
189 4
        }
190
191 4
        return false;
192
    }
193
194
    /**
195
     * Extracts the exception message.
196
     *
197
     * @param FlattenException $exception
198
     *
199
     * @return string Message
200
     */
201 4
    protected function getExceptionMessage($exception)
202
    {
203 4
        $showExceptionMessage = $this->isSubclassOf($exception, $this->exceptionMessages);
204
205 4
        if ($showExceptionMessage || $this->showException) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $showExceptionMessage of type integer|false is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
206 4
            return $exception->getMessage();
207
        }
208
209
        $statusCode = $this->getStatusCode($exception);
210
211
        return array_key_exists($statusCode, Response::$statusTexts) ? Response::$statusTexts[$statusCode] : 'error';
212
    }
213
214
    /**
215
     * Determines the status code to use for the response.
216
     *
217
     * @param FlattenException $exception
218
     *
219
     * @return int
220
     */
221 4
    protected function getStatusCode($exception)
222
    {
223 4
        $isExceptionMappedToStatusCode = $this->isSubclassOf($exception, $this->exceptionCodes);
224
225 4
        return $isExceptionMappedToStatusCode ?: $exception->getStatusCode();
226
    }
227
228
    /**
229
     * Determines the format to use for the response.
230
     *
231
     * @param Request $request
232
     * @param string  $format
233
     *
234
     * @return string
235
     */
236 4
    protected function getFormat(Request $request, $format)
237
    {
238
        try {
239 4
            $accept = $this->formatNegotiator->getBest('', []);
240 4
            if ($accept) {
241
                $format = $request->getFormat($accept->getType());
242
            }
243 4
            $request->attributes->set('_format', $format);
244 4
        } catch (StopFormatListenerException $e) {
245
            $format = $request->getRequestFormat();
246
        }
247
248 4
        return $format;
249
    }
250
251
    /**
252
     * Determines the parameters to pass to the view layer.
253
     *
254
     * Overwrite it in a custom ExceptionController class to add additionally parameters
255
     * that should be passed to the view layer.
256
     *
257
     * @param string               $currentContent
258
     * @param int                  $code
259
     * @param FlattenException     $exception
260
     * @param DebugLoggerInterface $logger
261
     * @param string               $format
262
     *
263
     * @return array
264
     */
265 4
    protected function getParameters($currentContent, $code, $exception, DebugLoggerInterface $logger = null, $format = 'html')
266
    {
267
        return [
268 4
            'status' => 'error',
269 4
            'status_code' => $code,
270 4
            'status_text' => array_key_exists($code, Response::$statusTexts) ? Response::$statusTexts[$code] : 'error',
271 4
            'currentContent' => $currentContent,
272 4
            'message' => $this->getExceptionMessage($exception),
273 4
            'exception' => $exception,
274 4
        ];
275
    }
276
}
277