Completed
Push — master ( 933a2a...d1c0c8 )
by Lukas Kahwe
05:29
created

ExceptionController::createExceptionWrapper()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
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\ExceptionWrapper;
16
use FOS\RestBundle\Util\StopFormatListenerException;
17
use FOS\RestBundle\View\ExceptionWrapperHandlerInterface;
18
use FOS\RestBundle\View\View;
19
use FOS\RestBundle\View\ViewHandlerInterface;
20
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
21
use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference;
22
use Symfony\Component\HttpFoundation\Request;
23
use Symfony\Component\HttpFoundation\Response;
24
use Symfony\Component\Debug\Exception\FlattenException;
25
use Symfony\Component\HttpKernel\Log\DebugLoggerInterface;
26
27
/**
28
 * Custom ExceptionController that uses the view layer and supports HTTP response status code mapping.
29
 */
30
class ExceptionController
31
{
32
    private $exceptionWrapperHandler;
33
    private $formatNegotiator;
34
    private $viewHandler;
35
    private $templating;
36
    private $exceptionCodes;
37
    private $exceptionMessages;
38
    private $showException;
39
40 4
    public function __construct(
41
        ExceptionWrapperHandlerInterface $exceptionWrapperHandler,
42
        FormatNegotiator $formatNegotiator,
43
        ViewHandlerInterface $viewHandler,
44
        EngineInterface $templating,
45
        array $exceptionCodes,
46
        array $exceptionMessages,
47
        $showException
48
    ) {
49 4
        $this->exceptionWrapperHandler = $exceptionWrapperHandler;
50 4
        $this->formatNegotiator = $formatNegotiator;
51 4
        $this->viewHandler = $viewHandler;
52 4
        $this->templating = $templating;
53 4
        $this->exceptionCodes = $exceptionCodes;
54 4
        $this->exceptionMessages = $exceptionMessages;
55 4
        $this->showException = $showException;
56 4
    }
57
58
    /**
59
     * Creates a new ExceptionWrapper instance that can be overwritten by a custom
60
     * ExceptionController class.
61
     *
62
     * @param array $parameters Template parameters
63
     *
64
     * @return ExceptionWrapper ExceptionWrapper instance
65
     */
66 4
    protected function createExceptionWrapper(array $parameters)
67
    {
68 4
        return $this->exceptionWrapperHandler->wrap($parameters);
69
    }
70
71
    /**
72
     * Converts an Exception to a Response.
73
     *
74
     * @param Request              $request
75
     * @param FlattenException     $exception
76
     * @param DebugLoggerInterface $logger
77
     *
78
     * @throws \InvalidArgumentException
79
     *
80
     * @return Response
81
     */
82 4
    public function showAction(Request $request, FlattenException $exception, DebugLoggerInterface $logger = null)
83
    {
84
        try {
85 4
            $format = $this->getFormat($request, $request->getRequestFormat());
86 4
        } catch (\Exception $e) {
87
            $format = null;
88
        }
89 4
        if (null === $format) {
90
            $message = 'No matching accepted Response format could be determined, while handling: ';
91
            $message .= $this->getExceptionMessage($exception);
92
93
            return $this->createPlainResponse($message, Response::HTTP_NOT_ACCEPTABLE, $exception->getHeaders());
94
        }
95
96 4
        $currentContent = $this->getAndCleanOutputBuffering(
97 4
            $request->headers->get('X-Php-Ob-Level', -1)
98 4
        );
99 4
        $code = $this->getStatusCode($exception);
100 4
        $parameters = $this->getParameters($this->viewHandler, $currentContent, $code, $exception, $logger, $format);
101 4
        $showException = $request->attributes->get('showException', $this->showException);
102
103
        try {
104 4
            if (!$this->viewHandler->isFormatTemplating($format)) {
105 4
                $parameters = $this->createExceptionWrapper($parameters);
106 4
            }
107
108 4
            $view = View::create($parameters, $code, $exception->getHeaders());
109 4
            $view->setFormat($format);
110
111 4
            if ($this->viewHandler->isFormatTemplating($format)) {
112
                $view->setTemplate($this->findTemplate($request, $format, $code, $showException));
113
            }
114
115 4
            $response = $this->viewHandler->handle($view);
116 4
        } catch (\Exception $e) {
117
            $message = 'An Exception was thrown while handling: ';
118
            $message .= $this->getExceptionMessage($exception);
119
            $response = $this->createPlainResponse($message, Response::HTTP_INTERNAL_SERVER_ERROR, $exception->getHeaders());
120
        }
121
122 4
        return $response;
123
    }
124
125
    /**
126
     * Returns a Response Object with content type text/plain.
127
     *
128
     * @param string $content
129
     * @param int    $status
130
     * @param array  $headers
131
     *
132
     * @return Response
133
     */
134
    private function createPlainResponse($content, $status, $headers)
135
    {
136
        $headers['content-type'] = 'text/plain';
137
138
        return new Response($content, $status, $headers);
139
    }
140
141
    /**
142
     * Gets and cleans any content that was already outputted.
143
     *
144
     * This code comes from Symfony and should be synchronized on a regular basis
145
     * see src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php
146
     *
147
     * @return string
148
     */
149 4
    private function getAndCleanOutputBuffering($startObLevel)
150
    {
151 4
        if (ob_get_level() <= $startObLevel) {
152 4
            return '';
153
        }
154
        Response::closeOutputBuffers($startObLevel + 1, true);
155
156
        return ob_get_clean();
157
    }
158
159
    /**
160
     * Extracts the exception message.
161
     *
162
     * @param FlattenException $exception
163
     * @param array            $exceptionMap
164
     *
165
     * @return int|false
166
     */
167 4
    private function isSubclassOf($exception, $exceptionMap)
168
    {
169 4
        $exceptionClass = $exception->getClass();
170 4
        $reflectionExceptionClass = new \ReflectionClass($exceptionClass);
171
        try {
172 4
            foreach ($exceptionMap as $exceptionMapClass => $value) {
173
                if ($value
174
                    && ($exceptionClass === $exceptionMapClass || $reflectionExceptionClass->isSubclassOf($exceptionMapClass))
175
                ) {
176
                    return $value;
177
                }
178 4
            }
179 4
        } catch (\ReflectionException $re) {
180
            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...
181
                    .$re->getMessage();
182
        }
183
184 4
        return false;
185
    }
186
187
    /**
188
     * Extracts the exception message.
189
     *
190
     * @param FlattenException $exception
191
     *
192
     * @return string Message
193
     */
194 4
    protected function getExceptionMessage($exception)
195
    {
196 4
        $showExceptionMessage = $this->isSubclassOf($exception, $this->exceptionMessages);
197
198 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...
199 4
            return $exception->getMessage();
200
        }
201
202
        $statusCode = $this->getStatusCode($exception);
203
204
        return array_key_exists($statusCode, Response::$statusTexts) ? Response::$statusTexts[$statusCode] : 'error';
205
    }
206
207
    /**
208
     * Determines the status code to use for the response.
209
     *
210
     * @param FlattenException $exception
211
     *
212
     * @return int
213
     */
214 4
    protected function getStatusCode($exception)
215
    {
216 4
        $isExceptionMappedToStatusCode = $this->isSubclassOf($exception, $this->exceptionCodes);
217
218 4
        return $isExceptionMappedToStatusCode ?: $exception->getStatusCode();
219
    }
220
221
    /**
222
     * Determines the format to use for the response.
223
     *
224
     * @param Request $request
225
     * @param string  $format
226
     *
227
     * @return string
228
     */
229 4
    protected function getFormat(Request $request, $format)
230
    {
231
        try {
232 4
            $accept = $this->formatNegotiator->getBest('', []);
233 4
            if ($accept) {
234
                $format = $request->getFormat($accept->getType());
235
            }
236 4
            $request->attributes->set('_format', $format);
237 4
        } catch (StopFormatListenerException $e) {
238
            $format = $request->getRequestFormat();
239
        }
240
241 4
        return $format;
242
    }
243
244
    /**
245
     * Determines the parameters to pass to the view layer.
246
     *
247
     * Overwrite it in a custom ExceptionController class to add additionally parameters
248
     * that should be passed to the view layer.
249
     *
250
     * @param ViewHandlerInterface $viewHandler
251
     * @param string               $currentContent
252
     * @param int                  $code
253
     * @param FlattenException     $exception
254
     * @param DebugLoggerInterface $logger
255
     * @param string               $format
256
     *
257
     * @return array
258
     */
259 4
    protected function getParameters(ViewHandlerInterface $viewHandler, $currentContent, $code, $exception, DebugLoggerInterface $logger = null, $format = 'html')
260
    {
261
        $parameters = [
262 4
            'status' => 'error',
263 4
            'status_code' => $code,
264 4
            'status_text' => array_key_exists($code, Response::$statusTexts) ? Response::$statusTexts[$code] : 'error',
265 4
            'currentContent' => $currentContent,
266 4
            'message' => $this->getExceptionMessage($exception),
267 4
            'exception' => $exception,
268 4
        ];
269
270 4
        if ($viewHandler->isFormatTemplating($format)) {
271
            $parameters['logger'] = $logger;
272
        }
273
274 4
        return $parameters;
275
    }
276
277
    /**
278
     * Finds the template for the given format and status code.
279
     *
280
     * Note this method needs to be overridden in case another
281
     * engine than Twig should be supported;
282
     *
283
     * This code is inspired by TwigBundle and should be synchronized on a regular basis
284
     * see src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php
285
     *
286
     * @param Request $request
287
     * @param string  $format
288
     * @param int     $statusCode
289
     * @param bool    $showException
290
     *
291
     * @return TemplateReference
292
     */
293
    private function findTemplate(Request $request, $format, $statusCode, $showException)
294
    {
295
        $name = $showException ? 'exception' : 'error';
296
        if ($showException && 'html' == $format) {
297
            $name = 'exception_full';
298
        }
299
300
        // when not in debug, try to find a template for the specific HTTP status code and format
301
        if (!$showException) {
302
            $template = new TemplateReference('TwigBundle', 'Exception', $name.$statusCode, $format, 'twig');
303
            if ($this->templating->exists($template)) {
304
                return $template;
305
            }
306
        }
307
308
        // try to find a template for the given format
309
        $template = new TemplateReference('TwigBundle', 'Exception', $name, $format, 'twig');
310
        if ($this->templating->exists($template)) {
311
            return $template;
312
        }
313
314
        // default to a generic HTML exception
315
        $request->setRequestFormat('html');
316
317
        return new TemplateReference('TwigBundle', 'Exception', $showException ? 'exception_full' : $name, 'html', 'twig');
318
    }
319
}
320