Completed
Push — master ( 724ebd...bb6c14 )
by Marcin
18s queued 11s
created

ExceptionHandlerHelper   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 220
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 19
Bugs 0 Features 0
Metric Value
eloc 87
c 19
b 0
f 0
dl 0
loc 220
rs 10
ccs 65
cts 65
cp 1
wmc 24

7 Methods

Rating   Name   Duplication   Size   Complexity  
A render() 0 27 4
A getExceptionHandlerConfig() 0 5 1
A getExceptionHandlerDefaultConfig() 0 24 1
A unauthenticated() 0 8 1
A getErrorMessageForException() 0 13 3
B processException() 0 39 8
B error() 0 46 6
1
<?php
2
declare(strict_types=1);
3
4
namespace MarcinOrlowski\ResponseBuilder;
5
6
/**
7
 * Exception handler using ResponseBuilder to return JSON even in such hard tines
8
 *
9
 * @package   MarcinOrlowski\ResponseBuilder
10
 *
11
 * @author    Marcin Orlowski <mail (#) marcinOrlowski (.) com>
12
 * @copyright 2016-2019 Marcin Orlowski
13
 * @license   http://www.opensource.org/licenses/mit-license.php MIT
14
 * @link      https://github.com/MarcinOrlowski/laravel-api-response-builder
15
 */
16
17
use Exception;
18
use Illuminate\Auth\AuthenticationException as AuthException;
19
use Illuminate\Support\Facades\Config;
20
use Illuminate\Support\Facades\Lang;
21
use Illuminate\Validation\ValidationException;
22
use Symfony\Component\HttpFoundation\Response as HttpResponse;
23
use Symfony\Component\HttpKernel\Exception\HttpException;
24
25
/**
26
 * Class ExceptionHandlerHelper
27
 */
28
class ExceptionHandlerHelper
29
{
30
    /**
31
     * Render an exception into valid API response.
32
     *
33
     * @param \Illuminate\Http\Request $request Request object
34
     * @param \Exception               $ex      Exception
35
     *
36
     * @return HttpResponse
37
     */
38
    public static function render(/** @scrutinizer ignore-unused */ $request, Exception $ex): HttpResponse
39
    {
40
        $result = null;
41
        $cfg = static::getExceptionHandlerConfig()['map'];
42
43
        if ($ex instanceof HttpException) {
44
            // Check if we have any exception configuration for this particular Http status code.
45
            // This confing entry is guaranted to exist (at least 'default'). Enforced by tests.
46
            $http_code = $ex->getStatusCode();
47
            $ex_cfg = $cfg[ HttpException::class ][ $http_code ] ?? null;
48
            $ex_cfg = $ex_cfg ?? $cfg[ HttpException::class ]['default'];
49 2
            $result = self::processException($ex, $ex_cfg, $http_code);
50
        } elseif ($ex instanceof ValidationException) {
51 2
            // This entry is guaranted to exist. Enforced by tests.
52
            $http_code = HttpResponse::HTTP_UNPROCESSABLE_ENTITY;
53 2
            $ex_cfg = $cfg[ HttpException::class ][ $http_code ];
54 1
            $result = self::processException($ex, $ex_cfg, $http_code);
55 1
        }
56 1
57 1
        if ($result === null) {
58 1
            // This entry is guaranted to exist. Enforced by tests.
59
            $http_code = HttpResponse::HTTP_INTERNAL_SERVER_ERROR;
60 1
            $ex_cfg = $cfg['default'];
61 1
            $result = self::processException($ex, $ex_cfg, $http_code);
62 1
        }
63 1
64
        return $result;
65 1
    }
66 1
67 1
    protected static function processException(\Exception $ex, array $ex_cfg, int $http_code)
68 1
    {
69
        $api_code = $ex_cfg['api_code'];
70
        $http_code = $ex_cfg['http_code'] ?? $http_code;
71 1
        $msg_key = $ex_cfg['msg_key'] ?? null;
72 1
        $msg_enforce = $ex_cfg['msg_enforce'] ?? false;
73 1
74
        // No message key, let's get exception message and if there's nothing useful, fallback to built-in one.
75 2
        $error_message = $ex->getMessage();
76 1
        $placeholders = [
77 1
            'api_code' => $api_code,
78
            'message'  => ($error_message !== '') ? $error_message : '???',
79
        ];
80 2
81 2
        // shall we enforce error message?
82 2
        if ($msg_enforce) {
83
            // yes, please.
84
            if ($msg_key === null) {
85 2
                // there's no msg_key configured for this exact code, so let's obtain our default message
86
                $error_message = ($msg_key === null) ? static::getErrorMessageForException($ex, $http_code, $placeholders) : Lang::get($msg_key, $placeholders);
87
            }
88
        } else {
89
            // nothing enforced, handling pipeline: ex_message -> user_defined msg -> http_ex -> default
90
            if ($error_message === '') {
91
                $error_message = ($msg_key === null) ? static::getErrorMessageForException($ex, $http_code, $placeholders) : Lang::get($msg_key, $placeholders);
92
            }
93
        }
94
95
        // Lets' try to build the error response with what we have now
96 1
        $result = static::error($ex, $api_code, $http_code, $error_message);
97
98 1
        if ($result === null) {
99
            $ex_cfg = $cfg[ HttpException::class ][ $http_code ];
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $cfg seems to be never defined.
Loading history...
100
            $api_code = $ex_cfg['api_code'] ?? BaseApiCodes::EX_VALIDATION_EXCEPTION();
0 ignored issues
show
Deprecated Code introduced by
The function MarcinOrlowski\ResponseB..._VALIDATION_EXCEPTION() has been deprecated: Configure Exception Handler to use your own API code. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

100
            $api_code = $ex_cfg['api_code'] ?? /** @scrutinizer ignore-deprecated */ BaseApiCodes::EX_VALIDATION_EXCEPTION();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
101
            $http_code = $ex_cfg['http_code'] ?? $http_code;
102
            $result = static::error($ex, $api_code, $http_code, $error_message);
103
        }
104
105
        return $result;
106
    }
107
108
    /**
109
     * Returns error message for given exception. If exception message is empty, then falls back to
110
     * `default` handler either for HttpException (if $ex is instance of it), or generic `default`
111 5
     * config.
112
     *
113
     * @param \Exception $ex
114
     * @param int        $http_code
115 5
     * @param array      $placeholders
116
     *
117
     * @return string
118
     */
119 5
    protected static function getErrorMessageForException(\Exception $ex, int $http_code, array $placeholders): string
120 5
    {
121
        // exception message is uselss, lets go deeper
122
        if ($ex instanceof HttpException) {
123 5
            $error_message = Lang::get("response-builder::builder.http_{$http_code}", $placeholders);
124
        } else {
125 2
            // Still got nothing? Fall back to built-in generic message for this type of exception.
126 2
            $key = BaseApiCodes::getCodeMessageKey(($ex instanceof HttpException)
127 2
                ? BaseApiCodes::EX_HTTP_EXCEPTION() : BaseApiCodes::NO_ERROR_MESSAGE());
128
            $error_message = Lang::get($key, $placeholders);
129
        }
130
131 5
        return $error_message;
132 1
    }
133
134
    /**
135
     * Convert an authentication exception into an unauthenticated response.
136
     *
137 5
     * @param \Illuminate\Http\Request                 $request
138 5
     * @param \Illuminate\Auth\AuthenticationException $exception
139 5
     *
140 5
     * @return HttpResponse
141 5
     */
142 5
    protected function unauthenticated(/** @scrutinizer ignore-unused */ $request,
143
                                                                         AuthException $exception): HttpResponse
144 5
    {
145 5
        // This entry is guaranted to exist. Enforced by tests.
146 5
        $http_code = HttpResponse::HTTP_UNAUTHORIZED;
147 5
        $cfg = static::getExceptionHandlerConfig()['map'][ HttpException::class ][ $http_code ];
148 5
149
        return static::processException($exception, $cfg, $http_code);
150
    }
151
152
    /**
153 5
     * Process single error and produce valid API response.
154 5
     *
155
     * @param Exception $ex Exception to be handled.
156 1
     * @param integer   $api_code
157
     * @param integer   $http_code
158
     *
159 5
     * @return HttpResponse
160
     */
161
    protected static function error(Exception $ex,
162 5
                                    int $api_code, int $http_code = null, string $error_message): HttpResponse
163
    {
164
        $ex_http_code = ($ex instanceof HttpException) ? $ex->getStatusCode() : $ex->getCode();
165
        $http_code = $http_code ?? $ex_http_code;
166 5
167 4
        // Check if we now have valid HTTP error code for this case or need to make one up.
168 4
        // We cannot throw any exception if codes are invalid because we are in Exception Handler already.
169 4
        if ($http_code < ResponseBuilder::ERROR_HTTP_CODE_MIN) {
170
            // Not a valid code, let's try to get the exception status.
171
            $http_code = $ex_http_code;
172
        }
173
        // Can it be considered a valid HTTP error code?
174
        if ($http_code < ResponseBuilder::ERROR_HTTP_CODE_MIN) {
175 5
            // We now handle uncaught exception, so we cannot throw another one if there's
176 5
            // something wrong with the configuration, so we try to recover and use built-in
177
            // codes instead.
178 1
            // FIXME: We should log this event as (warning or error?)
179 1
            $http_code = ResponseBuilder::DEFAULT_HTTP_CODE_ERROR;
180 1
        }
181 1
182
        // If we have trace data debugging enabled, let's gather some debug info and add to the response.
183
        $debug_data = null;
184
        if (Config::get(ResponseBuilder::CONF_KEY_DEBUG_EX_TRACE_ENABLED, false)) {
185
            $debug_data = [
186 5
                Config::get(ResponseBuilder::CONF_KEY_DEBUG_EX_TRACE_KEY, ResponseBuilder::KEY_TRACE) => [
187
                    ResponseBuilder::KEY_CLASS => get_class($ex),
188
                    ResponseBuilder::KEY_FILE  => $ex->getFile(),
189
                    ResponseBuilder::KEY_LINE  => $ex->getLine(),
190
                ],
191
            ];
192
        }
193
194
        // If this is ValidationException, add all the messages from MessageBag to the data node.
195
        $data = null;
196
        if ($ex instanceof ValidationException) {
197
            /** @var ValidationException $ex */
198
            $data = [ResponseBuilder::KEY_MESSAGES => $ex->validator->errors()->messages()];
199
        }
200
201
        return ResponseBuilder::asError($api_code)
202
            ->withMessage($error_message)
203
            ->withHttpCode($http_code)
204
            ->withData($data)
205
            ->withDebugData($debug_data)
206
            ->build();
207
    }
208
209
    protected
210
    static function getExceptionHandlerDefaultConfig(): array
211
    {
212
        return [
213
            'map' => [
214
                HttpException::class => [
215
                    // used by unauthenticated() to obtain api and http code for the exception
216
                    HttpResponse::HTTP_UNAUTHORIZED         => [
217
                        'api_code' => BaseApiCodes::EX_AUTHENTICATION_EXCEPTION(),
0 ignored issues
show
Deprecated Code introduced by
The function MarcinOrlowski\ResponseB...HENTICATION_EXCEPTION() has been deprecated: Configure Exception Handler to use your own API code. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

217
                        'api_code' => /** @scrutinizer ignore-deprecated */ BaseApiCodes::EX_AUTHENTICATION_EXCEPTION(),

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
218
                    ],
219
                    // Required by ValidationException handler
220
                    HttpResponse::HTTP_UNPROCESSABLE_ENTITY => [
221
                        'api_code' => BaseApiCodes::EX_VALIDATION_EXCEPTION(),
0 ignored issues
show
Deprecated Code introduced by
The function MarcinOrlowski\ResponseB..._VALIDATION_EXCEPTION() has been deprecated: Configure Exception Handler to use your own API code. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

221
                        'api_code' => /** @scrutinizer ignore-deprecated */ BaseApiCodes::EX_VALIDATION_EXCEPTION(),

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
222
                    ],
223
                    // default handler is mandatory. `default` entry MUST have both `api_code` and `http_code` set.
224
                    'default'                               => [
225
                        'api_code'  => BaseApiCodes::EX_HTTP_EXCEPTION(),
226
                        'http_code' => HttpResponse::HTTP_BAD_REQUEST,
227
                    ],
228
                ],
229
                // default handler is mandatory. `default` entry MUST have both `api_code` and `http_code` set.
230
                'default'            => [
231
                    'api_code'  => BaseApiCodes::EX_UNCAUGHT_EXCEPTION(),
232
                    'http_code' => HttpResponse::HTTP_INTERNAL_SERVER_ERROR,
233
                ],
234
            ],
235
        ];
236
    }
237
238
    /**
239
     * Returns ExceptionHandlerHelper configration array with user configuration merged into built-in defaults.
240
     *
241
     * @return array
242
     */
243
    protected
244
    static function getExceptionHandlerConfig(): array
245
    {
246
        return Util::mergeConfig(static::getExceptionHandlerDefaultConfig(),
247
            \Config::get(ResponseBuilder::CONF_KEY_EXCEPTION_HANDLER, []));
248
    }
249
250
}
251