Completed
Push — master ( bb6c14...7b9d4f )
by Marcin
23s queued 11s
created

getExceptionHandlerDefaultConfig()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 13
c 2
b 0
f 0
nc 1
nop 0
dl 0
loc 23
ccs 0
cts 0
cp 0
crap 2
rs 9.8333
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, /** @scrutinizer ignore-type */ $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
            $result = self::processException($ex, $cfg[ HttpException::class ][ $http_code ], $http_code);
54 1
        }
55 1
56 1
        if ($result === null) {
57 1
            // This entry is guaranted to exist. Enforced by tests.
58 1
            $result = self::processException($ex, $cfg['default'], HttpResponse::HTTP_INTERNAL_SERVER_ERROR);
59
        }
60 1
61 1
        return $result;
62 1
    }
63 1
64
    /**
65 1
     * Handles given exception and produces valid HTTP response object.
66 1
     *
67 1
     * @param \Exception $ex                 Exception to be handled.
68 1
     * @param array      $ex_cfg             ExceptionHandler's config excerpt related to $ex exception type.
69
     * @param int        $fallback_http_code HTTP code to be assigned to produced $ex related response in
70
     *                                       case configuration array lacks own `http_code` value.
71 1
     *
72 1
     * @return \Symfony\Component\HttpFoundation\Response
73 1
     */
74
    protected static function processException(\Exception $ex, array $ex_cfg, int $fallback_http_code)
75 2
    {
76 1
        $api_code = $ex_cfg['api_code'];
77 1
        $http_code = $ex_cfg['http_code'] ?? $fallback_http_code;
78
        $msg_key = $ex_cfg['msg_key'] ?? null;
79
        $msg_enforce = $ex_cfg['msg_enforce'] ?? false;
80 2
81 2
        // No message key, let's get exception message and if there's nothing useful, fallback to built-in one.
82 2
        $msg = $ex->getMessage();
83
        $placeholders = [
84
            'api_code' => $api_code,
85 2
            'message'  => ($msg !== '') ? $msg : '???',
86
        ];
87
88
        // shall we enforce error message?
89
        if ($msg_enforce) {
90
            // yes, please.
91
            if ($msg_key === null) {
92
                // there's no msg_key configured for this exact code, so let's obtain our default message
93
                $msg = ($msg_key === null) ? static::getErrorMessageForException($ex, $http_code, $placeholders)
94
                    : Lang::get($msg_key, $placeholders);
95
            }
96 1
        } else {
97
            // nothing enforced, handling pipeline: ex_message -> user_defined_msg -> http_ex -> default
98 1
            if ($msg === '') {
99
                $msg = ($msg_key === null) ? static::getErrorMessageForException($ex, $http_code, $placeholders)
100
                    : Lang::get($msg_key, $placeholders);
101
            }
102
        }
103
104
        // Lets' try to build the error response with what we have now
105
        return static::error($ex, $api_code, $http_code, $msg);
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
                ? /** @scrutinizer ignore-deprecated */ BaseApiCodes::EX_HTTP_EXCEPTION()
128
                : /** @scrutinizer ignore-deprecated */ BaseApiCodes::NO_ERROR_MESSAGE());
129
            $error_message = Lang::get($key, $placeholders);
130
        }
131 5
132 1
        return $error_message;
133
    }
134
135
    /**
136
     * Convert an authentication exception into an unauthenticated response.
137 5
     *
138 5
     * @param \Illuminate\Http\Request                 $request
139 5
     * @param \Illuminate\Auth\AuthenticationException $exception
140 5
     *
141 5
     * @return HttpResponse
142 5
     */
143
    protected function unauthenticated(/** @scrutinizer ignore-unused */ $request,
144 5
                                                                         AuthException $exception): HttpResponse
145 5
    {
146 5
        // This entry is guaranted to exist. Enforced by tests.
147 5
        $http_code = HttpResponse::HTTP_UNAUTHORIZED;
148 5
        $cfg = static::getExceptionHandlerConfig()['map'][ HttpException::class ][ $http_code ];
149
150
        return static::processException($exception, $cfg, $http_code);
151
    }
152
153 5
    /**
154 5
     * Process single error and produce valid API response.
155
     *
156 1
     * @param Exception $ex Exception to be handled.
157
     * @param integer   $api_code
158
     * @param integer   $http_code
159 5
     *
160
     * @return HttpResponse
161
     */
162 5
    protected static function error(Exception $ex,
163
                                    int $api_code, int $http_code = null, string $error_message): HttpResponse
164
    {
165
        $ex_http_code = ($ex instanceof HttpException) ? $ex->getStatusCode() : $ex->getCode();
166 5
        $http_code = $http_code ?? $ex_http_code;
167 4
168 4
        // Check if we now have valid HTTP error code for this case or need to make one up.
169 4
        // We cannot throw any exception if codes are invalid because we are in Exception Handler already.
170
        if ($http_code < ResponseBuilder::ERROR_HTTP_CODE_MIN) {
171
            // Not a valid code, let's try to get the exception status.
172
            $http_code = $ex_http_code;
173
        }
174
        // Can it be considered a valid HTTP error code?
175 5
        if ($http_code < ResponseBuilder::ERROR_HTTP_CODE_MIN) {
176 5
            // We now handle uncaught exception, so we cannot throw another one if there's
177
            // something wrong with the configuration, so we try to recover and use built-in
178 1
            // codes instead.
179 1
            // FIXME: We should log this event as (warning or error?)
180 1
            $http_code = ResponseBuilder::DEFAULT_HTTP_CODE_ERROR;
181 1
        }
182
183
        // If we have trace data debugging enabled, let's gather some debug info and add to the response.
184
        $debug_data = null;
185
        if (Config::get(ResponseBuilder::CONF_KEY_DEBUG_EX_TRACE_ENABLED, false)) {
186 5
            $debug_data = [
187
                Config::get(ResponseBuilder::CONF_KEY_DEBUG_EX_TRACE_KEY, ResponseBuilder::KEY_TRACE) => [
188
                    ResponseBuilder::KEY_CLASS => get_class($ex),
189
                    ResponseBuilder::KEY_FILE  => $ex->getFile(),
190
                    ResponseBuilder::KEY_LINE  => $ex->getLine(),
191
                ],
192
            ];
193
        }
194
195
        // If this is ValidationException, add all the messages from MessageBag to the data node.
196
        $data = null;
197
        if ($ex instanceof ValidationException) {
198
            /** @var ValidationException $ex */
199
            $data = [ResponseBuilder::KEY_MESSAGES => $ex->validator->errors()->messages()];
200
        }
201
202
        return ResponseBuilder::asError($api_code)
203
            ->withMessage($error_message)
204
            ->withHttpCode($http_code)
205
            ->withData($data)
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type array<string,array>; however, parameter $data of MarcinOrlowski\ResponseB...onseBuilder::withData() does only seem to accept null, maybe add an additional type check? ( Ignorable by Annotation )

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

205
            ->withData(/** @scrutinizer ignore-type */ $data)
Loading history...
206
            ->withDebugData($debug_data)
207
            ->build();
208
    }
209
210
    /**
211
     * Returns default (built-in) exception handler config array.
212
     *
213
     * @return array
214
     */
215
    protected static function getExceptionHandlerDefaultConfig(): array
216
    {
217
        return [
218
            'map' => [
219
                HttpException::class => [
220
                    // used by unauthenticated() to obtain api and http code for the exception
221
                    HttpResponse::HTTP_UNAUTHORIZED         => [
222
                        'api_code' => /** @scrutinizer ignore-deprecated */ BaseApiCodes::EX_AUTHENTICATION_EXCEPTION(),
223
                    ],
224
                    // Required by ValidationException handler
225
                    HttpResponse::HTTP_UNPROCESSABLE_ENTITY => [
226
                        'api_code' => /** @scrutinizer ignore-deprecated */ BaseApiCodes::EX_VALIDATION_EXCEPTION(),
227
                    ],
228
                    // default handler is mandatory. `default` entry MUST have both `api_code` and `http_code` set.
229
                    'default'                               => [
230
                        'api_code'  => /** @scrutinizer ignore-deprecated */ BaseApiCodes::EX_HTTP_EXCEPTION(),
231
                        'http_code' => HttpResponse::HTTP_BAD_REQUEST,
232
                    ],
233
                ],
234
                // default handler is mandatory. `default` entry MUST have both `api_code` and `http_code` set.
235
                'default'            => [
236
                    'api_code'  => /** @scrutinizer ignore-deprecated */ BaseApiCodes::EX_UNCAUGHT_EXCEPTION(),
237
                    'http_code' => HttpResponse::HTTP_INTERNAL_SERVER_ERROR,
238
                ],
239
            ],
240
        ];
241
    }
242
243
    /**
244
     * Returns ExceptionHandlerHelper configration array with user configuration merged into built-in defaults.
245
     *
246
     * @return array
247
     */
248
    protected static function getExceptionHandlerConfig(): array
249
    {
250
        return Util::mergeConfig(static::getExceptionHandlerDefaultConfig(),
251
            \Config::get(ResponseBuilder::CONF_KEY_EXCEPTION_HANDLER, []));
252
    }
253
254
}
255