ExceptionHandlerHelper::unauthenticated()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 9
ccs 3
cts 3
cp 1
crap 1
rs 10
c 2
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
4
namespace MarcinOrlowski\ResponseBuilder;
5
6
/**
7
 * Laravel API Response Builder
8
 *
9
 * @package   MarcinOrlowski\ResponseBuilder
10
 *
11
 * @author    Marcin Orlowski <mail (#) marcinOrlowski (.) com>
12
 * @copyright 2016-2021 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 Throwable;
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 MarcinOrlowski\ResponseBuilder\ExceptionHandlers\DefaultExceptionHandler;
23
use Symfony\Component\HttpFoundation\Response as HttpResponse;
24
use Symfony\Component\HttpKernel\Exception\HttpException;
25
use MarcinOrlowski\ResponseBuilder\ResponseBuilder as RB;
26
27
/**
28
 * Exception handler using ResponseBuilder to return JSON even in such hard tines
29
 */
30
class ExceptionHandlerHelper
31
{
32
	/**
33
	 * Render an exception into valid API response.
34
	 *
35
	 * @param \Illuminate\Http\Request $request Request object
36
	 * @param \Throwable               $ex      Throwable to handle
37
	 *
38
	 * @return HttpResponse
39 4
	 */
40
	public static function render(/** @scrutinizer ignore-unused */ $request, \Throwable $ex): HttpResponse
41 4
	{
42
		$result = null;
43 4
44
		$cfg = static::getHandler($ex);
45 4
		do {
46
			if ($cfg === null) {
47 2
				// Default handler MUST be present by design and always return something useful.
48
				$cfg = self::getExceptionHandlerConfig()[ RB::KEY_DEFAULT ];
49
			}
50 4
51 4
			$handler = new $cfg[ RB::KEY_HANDLER ]();
52 4
			$handler_result = $handler->handle($cfg[ RB::KEY_CONFIG ], $ex);
53 4
			if ($handler_result !== null) {
54
				$result = self::processException($ex, $handler_result);
55
			} else {
56
				// Let's fall back to default handler in next round.
57
				$cfg = null;
58 4
			}
59
		} while ($result === null);
60 4
61
		return $result;
62
	}
63
64
	/**
65
	 * Handles given throwable and produces valid HTTP response object.
66
	 *
67
	 * @param \Throwable $ex                 Throwable 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. Default
71
	 *                                       HttpResponse::HTTP_INTERNAL_SERVER_ERROR
72
	 *
73
	 * @return \Symfony\Component\HttpFoundation\Response
74 6
	 */
75
	protected static function processException(\Throwable $ex, array $ex_cfg,
76
	                                           int $fallback_http_code = HttpResponse::HTTP_INTERNAL_SERVER_ERROR)
77 6
	{
78 6
		$api_code = $ex_cfg['api_code'];
79 6
		$http_code = $ex_cfg['http_code'] ?? $fallback_http_code;
80 6
		$msg_key = $ex_cfg['msg_key'] ?? null;
81
		$msg_enforce = $ex_cfg['msg_enforce'] ?? false;
82
83 6
		// No message key, let's get exception message and if there's nothing useful, fallback to built-in one.
84
		$msg = $ex->getMessage();
85 6
		$placeholders = [
86 6
			'api_code' => $api_code,
87
			'message'  => ($msg !== '') ? $msg : '???',
88
		];
89
90 6
		// shall we enforce error message?
91
		if ($msg_enforce) {
92 1
			// yes, please.
93
			// there's no msg_key configured for this exact code, so let's obtain our default message
94 1
			$msg = ($msg_key === null) ? static::getErrorMessageForException($ex, $http_code, $placeholders)
95 1
				: Lang::get($msg_key, $placeholders);
96
		} else {
97
			// nothing enforced, handling pipeline: ex_message -> user_defined_msg -> http_ex -> default
98
			if ($msg === '') {
99 5
				$msg = ($msg_key === null) ? static::getErrorMessageForException($ex, $http_code, $placeholders)
100 2
					: Lang::get($msg_key, $placeholders);
101 2
			}
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 6
	}
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 \Throwable $ex
114
	 * @param int        $http_code
115
	 * @param array      $placeholders
116
	 *
117
	 * @return string
118
	 */
119
	protected static function getErrorMessageForException(\Throwable $ex, int $http_code, array $placeholders): string
120 2
	{
121
		// exception message is uselss, lets go deeper
122
		if ($ex instanceof HttpException) {
123 2
			$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
			$key = BaseApiCodes::getCodeMessageKey(($ex instanceof HttpException)
127 2
				? BaseApiCodes::EX_HTTP_EXCEPTION() : BaseApiCodes::NO_ERROR_MESSAGE());
128 2
			$error_message = Lang::get($key, $placeholders);
129 2
		}
130
131
		return $error_message;
132 2
	}
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
	protected function unauthenticated(/** @scrutinizer ignore-unused */ $request,
143 1
	                                                                     AuthException $exception): HttpResponse
144
	{
145
		$cfg = self::getExceptionHandlerConfig();
146 1
147
		// This config entry is guaranted to exist. Enforced by tests.
148
		$cfg = $cfg[ HttpException::class ][ RB::KEY_CONFIG ][ HttpResponse::HTTP_UNAUTHORIZED ];
149 1
150
		return static::processException($exception, $cfg, HttpResponse::HTTP_UNAUTHORIZED);
151 1
	}
152
153
	/**
154
	 * Process single error and produce valid API response.
155
	 *
156
	 * @param \Throwable $ex Exception to be handled.
157
	 * @param integer   $api_code
158
	 * @param integer   $http_code
159
	 * @param string    $error_message
160
	 *
161
	 * @return HttpResponse
162
	 */
163
	protected static function error(Throwable $ex,
164 9
	                                int $api_code, int $http_code = null, string $error_message = null): HttpResponse
165
	{
166
		$ex_http_code = ($ex instanceof HttpException) ? $ex->getStatusCode() : $ex->getCode();
167 9
		$http_code = $http_code ?? $ex_http_code;
168 9
		$error_message = $error_message ?? '';
169 9
170
		// Check if we now have valid HTTP error code for this case or need to make one up.
171
		// We cannot throw any exception if codes are invalid because we are in Exception Handler already.
172
		if ($http_code < RB::ERROR_HTTP_CODE_MIN) {
173 9
			// Not a valid code, let's try to get the exception status.
174
			$http_code = $ex_http_code;
175 2
		}
176
		// Can it be considered a valid HTTP error code?
177
		if ($http_code < RB::ERROR_HTTP_CODE_MIN) {
178 9
			// We now handle uncaught exception, so we cannot throw another one if there's
179
			// something wrong with the configuration, so we try to recover and use built-in
180
			// codes instead.
181
			// FIXME: We should log this event as (warning or error?)
182
			$http_code = RB::DEFAULT_HTTP_CODE_ERROR;
183 1
		}
184
185
		// If we have trace data debugging enabled, let's gather some debug info and add to the response.
186
		$debug_data = null;
187 9
		if (Config::get(RB::CONF_KEY_DEBUG_EX_TRACE_ENABLED, false)) {
188 9
			$debug_data = [
189
				Config::get(RB::CONF_KEY_DEBUG_EX_TRACE_KEY, RB::KEY_TRACE) => [
190 1
					RB::KEY_CLASS => \get_class($ex),
191 1
					RB::KEY_FILE  => $ex->getFile(),
192 1
					RB::KEY_LINE  => $ex->getLine(),
193 1
				],
194
			];
195
		}
196
197
		// If this is ValidationException, add all the messages from MessageBag to the data node.
198
		$data = null;
199 9
		if ($ex instanceof ValidationException) {
200 9
			/** @var ValidationException $ex */
201
			$data = [RB::KEY_MESSAGES => $ex->validator->errors()->messages()];
202 1
		}
203
204
		return RB::asError($api_code)
205 9
			->withMessage($error_message)
206 9
			->withHttpCode($http_code)
207 9
			->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

207
			->withData(/** @scrutinizer ignore-type */ $data)
Loading history...
208 9
			->withDebugData($debug_data)
209 9
			->build();
210 9
	}
211
212
	/**
213
	 * Returns ExceptionHandlerHelper configration array with user configuration merged into built-in defaults.
214
	 *
215
	 * @return array
216
	 */
217
	protected static function getExceptionHandlerConfig(): array
218 7
	{
219
		$default_config = [
220
			HttpException::class         => [
221
				'handler' => HttpExceptionHandler::class,
0 ignored issues
show
Bug introduced by
The type MarcinOrlowski\ResponseB...er\HttpExceptionHandler was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
222 7
				'pri'     => -100,
223
				'config'  => [
224
					// used by unauthenticated() to obtain api and http code for the exception
225
					HttpResponse::HTTP_UNAUTHORIZED         => [
226 7
						RB::KEY_API_CODE => BaseApiCodes::EX_AUTHENTICATION_EXCEPTION(),
227 7
					],
228
					// Required by ValidationException handler
229
					HttpResponse::HTTP_UNPROCESSABLE_ENTITY => [
230 7
						RB::KEY_API_CODE => BaseApiCodes::EX_VALIDATION_EXCEPTION(),
231 7
					],
232
233
					RB::KEY_DEFAULT => [
234 7
						RB::KEY_API_CODE  => BaseApiCodes::EX_UNCAUGHT_EXCEPTION(),
235 7
						RB::KEY_HTTP_CODE => HttpResponse::HTTP_INTERNAL_SERVER_ERROR,
236 7
					],
237
				],
238
				// default config is built into handler.
239
			],
240
241
			// default handler is mandatory. `default` entry MUST have both `api_code` and `http_code` set.
242
			RB::KEY_DEFAULT => [
243 7
				'handler' => DefaultExceptionHandler::class,
244
				'pri'     => -127,
245
				'config'  => [
246
					RB::KEY_API_CODE  => BaseApiCodes::EX_UNCAUGHT_EXCEPTION(),
247 7
					RB::KEY_HTTP_CODE => HttpResponse::HTTP_INTERNAL_SERVER_ERROR,
248 7
				],
249
			],
250
		];
251
252
		$cfg = Util::mergeConfig($default_config,
253 7
			\Config::get(RB::CONF_KEY_EXCEPTION_HANDLER, []));
254 7
		Util::sortArrayByPri($cfg);
255 7
256
		return $cfg;
257 7
	}
258
259
260
	/**
261
	 * Returns name of exception handler class, configured to process specified exception class or @null if no
262
	 * exception handler can be determined.
263
	 *
264
	 * @param string $cls Name of exception class to handle
265
	 *
266
	 * @return array|null
267
	 */
268
	protected static function getHandler(\Throwable $ex): ?array
269 4
	{
270
		$result = null;
271 4
272
		$cls = \get_class($ex);
273 4
		if (\is_string($cls)) {
0 ignored issues
show
introduced by
The condition is_string($cls) is always true.
Loading history...
274 4
			$cfg = self::getExceptionHandlerConfig();
275 4
276
			// check for exact class name match...
277
			if (\array_key_exists($cls, $cfg)) {
278 4
				$result = $cfg[ $cls ];
279 2
			} else {
280
				// no exact match, then lets try with `instanceof`
281
				// Config entries are already sorted by priority.
282
				foreach (\array_keys($cfg) as $class_name) {
283 2
					if ($ex instanceof $class_name) {
284 2
						$result = $cfg[ $class_name ];
285
						break;
286
					}
287
				}
288
			}
289
		}
290
291
		return $result;
292 4
	}
293
294
}
295