Passed
Push — dev ( f01fd5...67e3d9 )
by Marcin
03:55
created

ExceptionHandlerHelper::unauthenticated()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
c 3
b 0
f 0
nc 1
nop 2
dl 0
loc 8
ccs 4
cts 4
cp 1
crap 1
rs 10
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 4
    public static function render(/** @scrutinizer ignore-unused */ $request, Exception $ex): HttpResponse
39
    {
40 4
        $result = null;
41 4
        $cfg = static::getExceptionHandlerConfig()['map'];
42
43 4
        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 1
            $http_code = $ex->getStatusCode();
47 1
            $ex_cfg = $cfg[ HttpException::class ][ $http_code ] ?? null;
48 1
            $ex_cfg = $ex_cfg ?? $cfg[ HttpException::class ]['default'];
49 1
            $result = self::processException($ex, $ex_cfg, $http_code);
0 ignored issues
show
Bug introduced by
It seems like $ex_cfg can also be of type null; however, parameter $ex_cfg of MarcinOrlowski\ResponseB...per::processException() does only seem to accept array, 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

49
            $result = self::processException($ex, /** @scrutinizer ignore-type */ $ex_cfg, $http_code);
Loading history...
50 3
        } elseif ($ex instanceof ValidationException) {
51
            // This entry is guaranted to exist. Enforced by tests.
52 1
            $http_code = HttpResponse::HTTP_UNPROCESSABLE_ENTITY;
53 1
            $result = self::processException($ex, $cfg[ HttpException::class ][ $http_code ], $http_code);
54
        }
55
56 4
        if ($result === null) {
57
            // This entry is guaranted to exist. Enforced by tests.
58 2
            $result = self::processException($ex, $cfg['default'], HttpResponse::HTTP_INTERNAL_SERVER_ERROR);
59
        }
60
61 4
        return $result;
62
    }
63
64
    /**
65
     * Handles given exception and produces valid HTTP response object.
66
     *
67
     * @param \Exception $ex                 Exception to be handled.
68
     * @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
     *
72
     * @return \Symfony\Component\HttpFoundation\Response
73
     */
74 6
    protected static function processException(\Exception $ex, array $ex_cfg, int $fallback_http_code)
75
    {
76 6
        $api_code = $ex_cfg['api_code'];
77 6
        $http_code = $ex_cfg['http_code'] ?? $fallback_http_code;
78 6
        $msg_key = $ex_cfg['msg_key'] ?? null;
79 6
        $msg_enforce = $ex_cfg['msg_enforce'] ?? false;
80
81
        // No message key, let's get exception message and if there's nothing useful, fallback to built-in one.
82 6
        $msg = $ex->getMessage();
83
        $placeholders = [
84 6
            'api_code' => $api_code,
85 6
            'message'  => ($msg !== '') ? $msg : '???',
86
        ];
87
88
        // shall we enforce error message?
89 6
        if ($msg_enforce) {
90
            // yes, please.
91 1
            if ($msg_key === null) {
92
                // there's no msg_key configured for this exact code, so let's obtain our default message
93 1
                $msg = ($msg_key === null) ? static::getErrorMessageForException($ex, $http_code, $placeholders)
94 1
                    : Lang::get($msg_key, $placeholders);
95
            }
96
        } else {
97
            // nothing enforced, handling pipeline: ex_message -> user_defined_msg -> http_ex -> default
98 5
            if ($msg === '') {
99 2
                $msg = ($msg_key === null) ? static::getErrorMessageForException($ex, $http_code, $placeholders)
100 2
                    : Lang::get($msg_key, $placeholders);
101
            }
102
        }
103
104
        // Lets' try to build the error response with what we have now
105 6
        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
     * config.
112
     *
113
     * @param \Exception $ex
114
     * @param int        $http_code
115
     * @param array      $placeholders
116
     *
117
     * @return string
118
     */
119 3
    protected static function getErrorMessageForException(\Exception $ex, int $http_code, array $placeholders): string
120
    {
121
        // exception message is uselss, lets go deeper
122 3
        if ($ex instanceof HttpException) {
123 1
            $error_message = Lang::get("response-builder::builder.http_{$http_code}", $placeholders);
124
        } else {
125
            // 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());
0 ignored issues
show
Deprecated Code introduced by
The function MarcinOrlowski\ResponseB...es::EX_HTTP_EXCEPTION() has been deprecated: Configure Exception Handler to use your own API code. This method will be removed in v8. ( Ignorable by Annotation )

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

127
                ? /** @scrutinizer ignore-deprecated */ BaseApiCodes::EX_HTTP_EXCEPTION() : BaseApiCodes::NO_ERROR_MESSAGE());

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...
128 2
            $error_message = Lang::get($key, $placeholders);
129
        }
130
131 3
        return $error_message;
132
    }
133
134
    /**
135
     * Convert an authentication exception into an unauthenticated response.
136
     *
137
     * @param \Illuminate\Http\Request                 $request
138
     * @param \Illuminate\Auth\AuthenticationException $exception
139
     *
140
     * @return HttpResponse
141
     */
142 1
    protected function unauthenticated(/** @scrutinizer ignore-unused */ $request,
143
                                                                         AuthException $exception): HttpResponse
144
    {
145
        // This entry is guaranted to exist. Enforced by tests.
146 1
        $http_code = HttpResponse::HTTP_UNAUTHORIZED;
147 1
        $cfg = static::getExceptionHandlerConfig()['map'][ HttpException::class ][ $http_code ];
148
149 1
        return static::processException($exception, $cfg, $http_code);
150
    }
151
152
    /**
153
     * Process single error and produce valid API response.
154
     *
155
     * @param Exception $ex Exception to be handled.
156
     * @param integer   $api_code
157
     * @param integer   $http_code
158
     *
159
     * @return HttpResponse
160
     */
161 9
    protected static function error(Exception $ex,
162
                                    int $api_code, int $http_code = null, string $error_message): HttpResponse
163
    {
164 9
        $ex_http_code = ($ex instanceof HttpException) ? $ex->getStatusCode() : $ex->getCode();
165 9
        $http_code = $http_code ?? $ex_http_code;
166
167
        // Check if we now have valid HTTP error code for this case or need to make one up.
168
        // We cannot throw any exception if codes are invalid because we are in Exception Handler already.
169 9
        if ($http_code < ResponseBuilder::ERROR_HTTP_CODE_MIN) {
170
            // Not a valid code, let's try to get the exception status.
171 2
            $http_code = $ex_http_code;
172
        }
173
        // Can it be considered a valid HTTP error code?
174 9
        if ($http_code < ResponseBuilder::ERROR_HTTP_CODE_MIN) {
175
            // We now handle uncaught exception, so we cannot throw another one if there's
176
            // something wrong with the configuration, so we try to recover and use built-in
177
            // codes instead.
178
            // FIXME: We should log this event as (warning or error?)
179 1
            $http_code = ResponseBuilder::DEFAULT_HTTP_CODE_ERROR;
180
        }
181
182
        // If we have trace data debugging enabled, let's gather some debug info and add to the response.
183 9
        $debug_data = null;
184 9
        if (Config::get(ResponseBuilder::CONF_KEY_DEBUG_EX_TRACE_ENABLED, false)) {
185
            $debug_data = [
186 1
                Config::get(ResponseBuilder::CONF_KEY_DEBUG_EX_TRACE_KEY, ResponseBuilder::KEY_TRACE) => [
187 1
                    ResponseBuilder::KEY_CLASS => get_class($ex),
188 1
                    ResponseBuilder::KEY_FILE  => $ex->getFile(),
189 1
                    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 9
        $data = null;
196 9
        if ($ex instanceof ValidationException) {
197
            /** @var ValidationException $ex */
198 1
            $data = [ResponseBuilder::KEY_MESSAGES => $ex->validator->errors()->messages()];
199
        }
200
201 9
        return ResponseBuilder::asError($api_code)
202 9
            ->withMessage($error_message)
203 9
            ->withHttpCode($http_code)
204 9
            ->withData($data)
205 9
            ->withDebugData($debug_data)
206 9
            ->build();
207
    }
208
209
    /**
210
     * Returns default (built-in) exception handler config array.
211
     *
212
     * @return array
213
     */
214 7
    protected static function getExceptionHandlerDefaultConfig(): array
215
    {
216
        return [
217
            'map' => [
218
                HttpException::class => [
219
                    // used by unauthenticated() to obtain api and http code for the exception
220 7
                    HttpResponse::HTTP_UNAUTHORIZED         => [
221 7
                        '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. This method will be removed in v8. ( 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_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...
222
                    ],
223
                    // Required by ValidationException handler
224 7
                    HttpResponse::HTTP_UNPROCESSABLE_ENTITY => [
225 7
                        '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. This method will be removed in v8. ( Ignorable by Annotation )

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

225
                        '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...
226
                    ],
227
                    // default handler is mandatory. `default` entry MUST have both `api_code` and `http_code` set.
228
                    'default'                               => [
229 7
                        'api_code'  => BaseApiCodes::EX_HTTP_EXCEPTION(),
0 ignored issues
show
Deprecated Code introduced by
The function MarcinOrlowski\ResponseB...es::EX_HTTP_EXCEPTION() has been deprecated: Configure Exception Handler to use your own API code. This method will be removed in v8. ( Ignorable by Annotation )

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

229
                        'api_code'  => /** @scrutinizer ignore-deprecated */ BaseApiCodes::EX_HTTP_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...
230 7
                        'http_code' => HttpResponse::HTTP_BAD_REQUEST,
231
                    ],
232
                ],
233
                // default handler is mandatory. `default` entry MUST have both `api_code` and `http_code` set.
234
                'default'            => [
235 7
                    'api_code'  => BaseApiCodes::EX_UNCAUGHT_EXCEPTION(),
0 ignored issues
show
Deprecated Code introduced by
The function MarcinOrlowski\ResponseB...EX_UNCAUGHT_EXCEPTION() has been deprecated: Configure Exception Handler to use your own API code. This method will be removed in v8. ( Ignorable by Annotation )

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

235
                    'api_code'  => /** @scrutinizer ignore-deprecated */ BaseApiCodes::EX_UNCAUGHT_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...
236 7
                    'http_code' => HttpResponse::HTTP_INTERNAL_SERVER_ERROR,
237
                ],
238
            ],
239
        ];
240
    }
241
242
    /**
243
     * Returns ExceptionHandlerHelper configration array with user configuration merged into built-in defaults.
244
     *
245
     * @return array
246
     */
247 7
    protected static function getExceptionHandlerConfig(): array
248
    {
249 7
        return Util::mergeConfig(static::getExceptionHandlerDefaultConfig(),
250 7
            \Config::get(ResponseBuilder::CONF_KEY_EXCEPTION_HANDLER, []));
251
    }
252
253
}
254