Completed
Push — master ( d1c0c8...4b87d9 )
by Lukas Kahwe
06:48
created

ExceptionController::getExceptionMessage()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 4.5924

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 12
ccs 4
cts 6
cp 0.6667
rs 9.2
cc 4
eloc 6
nc 3
nop 1
crap 4.5924
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
        $reflectionExceptionClass = new \ReflectionClass($exceptionClass);
184
        try {
185 4
            foreach ($exceptionMap as $exceptionMapClass => $value) {
186
                if ($value
187
                    && ($exceptionClass === $exceptionMapClass || $reflectionExceptionClass->isSubclassOf($exceptionMapClass))
188
                ) {
189
                    return $value;
190
                }
191 4
            }
192 4
        } catch (\ReflectionException $re) {
193
            return 'FOSUserBundle: Invalid class in fos_res.exception.messages: '
0 ignored issues
show
Bug Best Practice introduced by
The return type of return 'FOSUserBundle: I... ' . $re->getMessage(); (string) is incompatible with the return type documented by FOS\RestBundle\Controlle...ontroller::isSubclassOf of type integer|false.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

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