Passed
Branch master (4ddde1)
by Marcin
09:17
created

ResponseBuilder::make()   B

Complexity

Conditions 8
Paths 10

Size

Total Lines 40
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 10
Bugs 0 Features 0
Metric Value
cc 8
eloc 22
c 10
b 0
f 0
nc 10
nop 9
dl 0
loc 40
rs 8.4444

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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-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 Illuminate\Support\Facades\Config;
18
use Illuminate\Support\Facades\Response;
19
use Symfony\Component\HttpFoundation\Response as HttpResponse;
20
21
22
/**
23
 * Builds standardized HttpResponse response object
24
 */
25
class ResponseBuilder
26
{
27
	/**
28
	 * Default HTTP code to be used with success responses
29
	 */
30
	public const DEFAULT_HTTP_CODE_OK = HttpResponse::HTTP_OK;
31
32
	/**
33
	 * Default HTTP code to be used with error responses
34
	 */
35
	public const DEFAULT_HTTP_CODE_ERROR = HttpResponse::HTTP_BAD_REQUEST;
36
37
	/**
38
	 * Min allowed HTTP code for errorXXX()
39
	 */
40
	public const ERROR_HTTP_CODE_MIN = 400;
41
42
	/**
43
	 * Max allowed HTTP code for errorXXX()
44
	 */
45
	public const ERROR_HTTP_CODE_MAX = 599;
46
47
	/**
48
	 * Configuration keys
49
	 */
50
	public const CONF_KEY_DEBUG_DEBUG_KEY        = 'response_builder.debug.debug_key';
51
	public const CONF_KEY_DEBUG_EX_TRACE_ENABLED = 'response_builder.debug.exception_handler.trace_enabled';
52
	public const CONF_KEY_DEBUG_EX_TRACE_KEY     = 'response_builder.debug.exception_handler.trace_key';
53
	public const CONF_KEY_MAP                    = 'response_builder.map';
54
	public const CONF_KEY_ENCODING_OPTIONS       = 'response_builder.encoding_options';
55
	public const CONF_KEY_CLASSES                = 'response_builder.classes';
56
	public const CONF_KEY_MIN_CODE               = 'response_builder.min_code';
57
	public const CONF_KEY_MAX_CODE               = 'response_builder.max_code';
58
	public const CONF_KEY_RESPONSE_KEY_MAP       = 'response_builder.map';
59
60
	/**
61
	 * Default keys to be used by exception handler while adding debug information
62
	 */
63
	public const KEY_DEBUG   = 'debug';
64
	public const KEY_TRACE   = 'trace';
65
	public const KEY_CLASS   = 'class';
66
	public const KEY_FILE    = 'file';
67
	public const KEY_LINE    = 'line';
68
	public const KEY_KEY     = 'key';
69
	public const KEY_METHOD  = 'method';
70
	public const KEY_SUCCESS = 'success';
71
	public const KEY_CODE    = 'code';
72
	public const KEY_LOCALE  = 'locale';
73
	public const KEY_MESSAGE = 'message';
74
	public const KEY_DATA    = 'data';
75
76
	/**
77
	 * Default key to be used by exception handler while processing ValidationException
78
	 * to return all the error messages
79
	 */
80
	public const KEY_MESSAGES = 'messages';
81
82
	/**
83
	 * Default JSON encoding options. Must be specified as final value (i.e. 271) and NOT
84
	 * exression i.e. `JSON_HEX_TAG|JSON_HEX_APOS|...` as such syntax is not yet supported
85
	 * by PHP.
86
	 *
87
	 * 271 = JSON_HEX_TAG|JSON_HEX_APOS|JSON_HEX_AMP|JSON_HEX_QUOT|JSON_UNESCAPED_UNICODE
88
	 */
89
	public const DEFAULT_ENCODING_OPTIONS = 271;
90
91
	/**
92
	 * Reads and validates "classes" config mapping
93
	 *
94
	 * @return array Classes mapping as specified in configuration or empty array if configuration found
95
	 *
96
	 * @throws \RuntimeException if "classes" mapping is technically invalid (i.e. not array etc).
97
	 */
98
	protected static function getClassesMapping(): ?array
99
	{
100
		$classes = Config::get(self::CONF_KEY_CLASSES);
101
102
		if ($classes !== null) {
103
			if (!is_array($classes)) {
104
				throw new \RuntimeException(
105
					sprintf('CONFIG: "classes" mapping must be an array (%s given)', gettype($classes)));
106
			}
107
108
			$mandatory_keys = [
109
				static::KEY_KEY,
110
				static::KEY_METHOD,
111
			];
112
			foreach ($classes as $class_name => $class_config) {
113
				foreach ($mandatory_keys as $key_name) {
114
					if (!array_key_exists($key_name, $class_config)) {
115
						throw new \RuntimeException("CONFIG: Missing '{$key_name}' for '{$class_name}' class mapping");
116
					}
117
				}
118
			}
119
		} else {
120
			$classes = [];
121
		}
122
123
		return $classes;
124
	}
125
126
	/**
127
	 * Returns success
128
	 *
129
	 * @param object|array|null $data             payload to be returned as 'data' node, @null if none
130
	 * @param integer|null      $api_code         API code to be returned with the response or @null for default `OK` code
131
	 * @param array|null        $lang_args        arguments passed to Lang if message associated with API code uses placeholders
132
	 * @param integer|null      $http_code        HTTP return code to be set for this response or @null for default (200)
133
	 * @param integer|null      $encoding_options see http://php.net/manual/en/function.json-encode.php or @null to use
134
	 *                                            config's value or defaults
135
	 *
136
	 * @return HttpResponse
137
	 */
138
	public static function success($data = null, $api_code = null, array $lang_args = null,
139
	                               int $http_code = null, int $encoding_options = null): HttpResponse
140
	{
141
		return static::buildSuccessResponse($data, $api_code, $lang_args, $http_code, $encoding_options);
142
	}
143
144
	/**
145
	 * Returns success
146
	 *
147
	 * @param integer|null $api_code  API code to be returned with the response or @null for default `OK` code
148
	 * @param array|null   $lang_args arguments passed to Lang if message associated with API code uses placeholders
149
	 * @param integer|null $http_code HTTP return code to be set for this response or @null for default (200)
150
	 *
151
	 * @return HttpResponse
152
	 */
153
	public static function successWithCode(int $api_code = null, array $lang_args = null, int $http_code = null): HttpResponse
154
	{
155
		return static::success(null, $api_code, $lang_args, $http_code);
156
	}
157
158
	/**
159
	 * Returns success with custom HTTP code
160
	 *
161
	 * @param integer|null $http_code HTTP return code to be set for this response. If @null is passed, falls back
162
	 *                                to DEFAULT_HTTP_CODE_OK.
163
	 *
164
	 * @return HttpResponse
165
	 */
166
	public static function successWithHttpCode(int $http_code = null): HttpResponse
167
	{
168
		return static::buildSuccessResponse(null, BaseApiCodes::OK(), [], $http_code);
169
	}
170
171
	/**
172
	 * @param object|array|null $data             payload to be returned as 'data' node, @null if none
173
	 * @param integer|null      $api_code         API code to be returned with the response or @null for `OK` code
174
	 * @param array|null        $lang_args        arguments passed to Lang if message associated with API code uses placeholders
175
	 * @param integer|null      $http_code        HTTP return code to be set for this response
176
	 * @param integer|null      $encoding_options see http://php.net/manual/en/function.json-encode.php or @null to use
177
	 *                                            config's value or defaults
178
	 *
179
	 * @return HttpResponse
180
	 *
181
	 * @throws \InvalidArgumentException Thrown when provided arguments are invalid.
182
	 */
183
	protected static function buildSuccessResponse($data = null, int $api_code = null, array $lang_args = null,
184
	                                               int $http_code = null, int $encoding_options = null): HttpResponse
185
	{
186
		$http_code = $http_code ?? static::DEFAULT_HTTP_CODE_OK;
187
		$api_code = $api_code ?? BaseApiCodes::OK();
188
189
		Validator::assertInt('api_code', $api_code);
190
		Validator::assertInt('http_code', $http_code);
191
		Validator::assertIntRange('http_code', $http_code, 200, 299);
192
193
		return static::make(true, $api_code, $api_code, $data, $http_code, $lang_args, null, $encoding_options);
194
	}
195
196
	/**
197
	 * Builds error Response object. Supports optional arguments passed to Lang::get() if associated error
198
	 * message uses placeholders as well as return data payload
199
	 *
200
	 * @param integer           $api_code         API code to be returned with the response
201
	 * @param array|null        $lang_args        arguments array passed to Lang::get() for messages with placeholders
202
	 * @param object|array|null $data             payload array to be returned in 'data' node or response object
203
	 * @param integer|null      $http_code        optional HTTP status code to be used with this response or @null for default
204
	 * @param integer|null      $encoding_options see http://php.net/manual/en/function.json-encode.php or @null to use
205
	 *                                            config's value or defaults
206
	 *
207
	 * @return HttpResponse
208
	 */
209
	public static function error(int $api_code, array $lang_args = null, $data = null, int $http_code = null,
210
	                             int $encoding_options = null): HttpResponse
211
	{
212
		return static::buildErrorResponse($data, $api_code, $http_code, $lang_args, $encoding_options);
213
	}
214
215
	/**
216
	 * @param integer           $api_code         API code to be returned with the response
217
	 * @param object|array|null $data             payload to be returned as 'data' node, @null if none
218
	 * @param array|null        $lang_args        arguments array passed to Lang::get() for messages with placeholders
219
	 * @param integer|null      $encoding_options see http://php.net/manual/en/function.json-encode.php or @null to use
220
	 *                                            config's value or defaults
221
	 *
222
	 * @return HttpResponse
223
	 */
224
	public static function errorWithData(int $api_code, $data, array $lang_args = null,
225
	                                     int $encoding_options = null): HttpResponse
226
	{
227
		return static::buildErrorResponse($data, $api_code, null, $lang_args, $encoding_options);
228
	}
229
230
	/**
231
	 * @param integer           $api_code         API code to be returned with the response
232
	 * @param object|array|null $data             payload to be returned as 'data' node, @null if none
233
	 * @param integer|null      $http_code        HTTP error code to be returned with this Cannot be @null
234
	 * @param array|null        $lang_args        arguments array passed to Lang::get() for messages with placeholders
235
	 * @param integer|null      $encoding_options see http://php.net/manual/en/function.json-encode.php or @null to use
236
	 *                                            config's value or defaults
237
	 *
238
	 * @return HttpResponse
239
	 *
240
	 * @throws \InvalidArgumentException if http_code is @null
241
	 */
242
	public static function errorWithDataAndHttpCode(int $api_code, $data, int $http_code, array $lang_args = null,
243
	                                                int $encoding_options = null): HttpResponse
244
	{
245
		return static::buildErrorResponse($data, $api_code, $http_code, $lang_args, $encoding_options);
246
	}
247
248
	/**
249
	 * @param integer      $api_code  API code to be returned with the response
250
	 * @param integer|null $http_code HTTP return code to be set for this response or @null for default
251
	 * @param array|null   $lang_args arguments array passed to Lang::get() for messages with placeholders
252
	 *
253
	 * @return HttpResponse
254
	 *
255
	 * @throws \InvalidArgumentException if http_code is @null
256
	 */
257
	public static function errorWithHttpCode(int $api_code, int $http_code, array $lang_args = null): HttpResponse
258
	{
259
		return static::buildErrorResponse(null, $api_code, $http_code, $lang_args);
260
	}
261
262
	/**
263
	 * @param integer           $api_code         API code to be returned with the response
264
	 * @param string            $error_message    custom message to be returned as part of error response
265
	 * @param object|array|null $data             payload to be returned as 'data' node, @null if none
266
	 * @param integer|null      $http_code        optional HTTP status code to be used with this response or @null for defaults
267
	 * @param integer|null      $encoding_options see http://php.net/manual/en/function.json-encode.php or @null to use config's
268
	 *                                            value or defaults
269
	 *
270
	 * @return HttpResponse
271
	 */
272
	public static function errorWithMessageAndData(int $api_code, string $error_message, $data,
273
	                                               int $http_code = null, int $encoding_options = null): HttpResponse
274
	{
275
		return static::buildErrorResponse($data, $api_code, $http_code, null,
276
			$error_message, null, $encoding_options);
277
	}
278
279
	/**
280
	 * @param integer           $api_code         API code to be returned with the response
281
	 * @param string            $error_message    custom message to be returned as part of error response
282
	 * @param object|array|null $data             payload to be returned as 'data' node, @null if none
283
	 * @param integer|null      $http_code        optional HTTP status code to be used with this response or @null for defaults
284
	 * @param integer|null      $encoding_options see http://php.net/manual/en/function.json-encode.php or @null to use
285
	 *                                            config's value or defaults
286
	 * @param array|null        $debug_data       optional debug data array to be added to returned JSON.
287
	 *
288
	 * @return HttpResponse
289
	 */
290
	public static function errorWithMessageAndDataAndDebug(int $api_code, string $error_message, $data,
291
	                                                       int $http_code = null, int $encoding_options = null,
292
	                                                       array $debug_data = null): HttpResponse
293
	{
294
		return static::buildErrorResponse($data, $api_code, $http_code, null,
295
			$error_message, null, $encoding_options, $debug_data);
296
	}
297
298
	/**
299
	 * @param integer      $api_code      API code to be returned with the response
300
	 * @param string       $error_message custom message to be returned as part of error response
301
	 * @param integer|null $http_code     optional HTTP status code to be used with this response or @null for defaults
302
	 *
303
	 * @return HttpResponse
304
	 */
305
	public static function errorWithMessage(int $api_code, string $error_message, int $http_code = null): HttpResponse
306
	{
307
		return static::buildErrorResponse(null, $api_code, $http_code, null, $error_message);
308
	}
309
310
	/**
311
	 * Builds error Response object. Supports optional arguments passed to Lang::get() if associated error message
312
	 * uses placeholders as well as return data payload
313
	 *
314
	 * @param object|array|null $data             payload array to be returned in 'data' node or response object or @null if none
315
	 * @param integer           $api_code         API code to be returned with the response
316
	 * @param integer|null      $http_code        optional HTTP status code to be used with this response or @null for default
317
	 * @param array|null        $lang_args        arguments array passed to Lang::get() for messages with placeholders
318
	 * @param string|null       $message          custom message to be returned as part of error response
319
	 * @param array|null        $headers          optional HTTP headers to be returned in error response
320
	 * @param integer|null      $encoding_options see see json_encode() docs for valid option values. Use @null to fall back to
321
	 *                                            config's value or defaults
322
	 * @param array|null        $debug_data       optional debug data array to be added to returned JSON.
323
	 *
324
	 * @return HttpResponse
325
	 *
326
	 * @throws \InvalidArgumentException Thrown if $code is not correct, outside the range, equals OK code etc.
327
	 *
328
	 * @noinspection MoreThanThreeArgumentsInspection
329
	 */
330
	protected static function buildErrorResponse($data, int $api_code, int $http_code = null, array $lang_args = null,
331
	                                             string $message = null, array $headers = null, int $encoding_options = null,
332
	                                             array $debug_data = null): HttpResponse
333
	{
334
		$http_code = $http_code ?? static::DEFAULT_HTTP_CODE_ERROR;
335
		$headers = $headers ?? [];
336
337
		$code_ok = BaseApiCodes::OK();
338
339
		Validator::assertInt('api_code', $api_code);
340
		if ($api_code !== $code_ok) {
341
			Validator::assertIntRange('api_code', $api_code, BaseApiCodes::getMinCode(), BaseApiCodes::getMaxCode());
342
		}
343
		if ($api_code === $code_ok) {
344
			throw new \InvalidArgumentException("Error response cannot use api_code of value  {$code_ok} which is reserved for OK");
345
		}
346
347
		Validator::assertInt('http_code', $http_code);
348
		Validator::assertIntRange('http_code', $http_code, static::ERROR_HTTP_CODE_MIN, static::ERROR_HTTP_CODE_MAX);
349
350
		$message_or_api_code = $message ?? $api_code;
351
352
		return static::make(false, $api_code, $message_or_api_code, $data, $http_code,
353
			$lang_args, $headers, $encoding_options, $debug_data);
354
	}
355
356
	/**
357
	 * @param boolean           $success             @true if response indicate success, @false otherwise
358
	 * @param integer           $api_code            API code to be returned with the response
359
	 * @param string|integer    $message_or_api_code message string or valid API code
360
	 * @param object|array|null $data                optional additional data to be included in response object
361
	 * @param integer|null      $http_code           return HTTP code for build Response object
362
	 * @param array|null        $lang_args           arguments array passed to Lang::get() for messages with placeholders
363
	 * @param array|null        $headers             optional HTTP headers to be returned in the response
364
	 * @param integer|null      $encoding_options    see http://php.net/manual/en/function.json-encode.php
365
	 * @param array|null        $debug_data          optional debug data array to be added to returned JSON.
366
	 *
367
	 * @return HttpResponse
368
	 *
369
	 * @throws \InvalidArgumentException If $api_code is neither a string nor valid integer code.
370
	 * @throws \InvalidArgumentException if $data is an object of class that is not configured in "classes" mapping.
371
	 *
372
	 * @noinspection MoreThanThreeArgumentsInspection
373
	 */
374
	protected static function make(bool $success, int $api_code, $message_or_api_code, $data = null,
375
	                               int $http_code = null, array $lang_args = null, array $headers = null,
376
	                               int $encoding_options = null, array $debug_data = null): HttpResponse
377
	{
378
		$headers = $headers ?? [];
379
		$http_code = $http_code ?? ($success ? static::DEFAULT_HTTP_CODE_OK : static::DEFAULT_HTTP_CODE_ERROR);
380
		$encoding_options = $encoding_options ?? Config::get(self::CONF_KEY_ENCODING_OPTIONS, static::DEFAULT_ENCODING_OPTIONS);
381
382
		Validator::assertInt('encoding_options', $encoding_options);
383
384
		Validator::assertInt('api_code', $api_code);
385
		if (!BaseApiCodes::isCodeValid($api_code)) {
386
			$min = BaseApiCodes::getMinCode();
387
			$max = BaseApiCodes::getMaxCode();
388
			throw new \InvalidArgumentException("API code value ({$api_code}) is out of allowed range {$min}-{$max}");
389
		}
390
391
		if (!(is_int($message_or_api_code) || is_string($message_or_api_code))) {
0 ignored issues
show
introduced by
The condition is_string($message_or_api_code) is always true.
Loading history...
392
			throw new \InvalidArgumentException(
393
				sprintf('Message must be either string or resolvable integer API code (%s given)',
394
					gettype($message_or_api_code))
395
			);
396
		}
397
398
		// we got code, not message string, so we need to check if we have the mapping for
399
		// this string already configured.
400
		if (is_int($message_or_api_code)) {
401
			$key = BaseApiCodes::getCodeMessageKey($message_or_api_code);
402
			if ($key === null) {
403
				// nope, let's get the default one instead
404
				$key = BaseApiCodes::getCodeMessageKey($success ? BaseApiCodes::OK() : BaseApiCodes::NO_ERROR_MESSAGE());
405
			}
406
407
			$lang_args = $lang_args ?? ['api_code' => $message_or_api_code];
408
			$message_or_api_code = \Lang::get($key, $lang_args);
409
		}
410
411
		return Response::json(
412
			static::buildResponse($success, $api_code, $message_or_api_code, $data, $debug_data),
413
			$http_code, $headers, $encoding_options
414
		);
415
	}
416
417
	/**
418
	 * Creates standardised API response array. If you set APP_DEBUG to true, 'code_hex' field will be
419
	 * additionally added to reported JSON for easier manual debugging.
420
	 *
421
	 * @param boolean           $success    @true if response indicates success, @false otherwise
422
	 * @param integer           $api_code   response code
423
	 * @param string            $message    message to return
424
	 * @param object|array|null $data       API response data if any
425
	 * @param array|null        $debug_data optional debug data array to be added to returned JSON.
426
	 *
427
	 * @return array response ready to be encoded as json and sent back to client
428
	 *
429
	 * @throws \RuntimeException in case of missing or invalid "classes" mapping configuration
430
	 */
431
	protected static function buildResponse(bool $success, int $api_code, string $message, $data = null,
432
	                                        array $debug_data = null): array
433
	{
434
		// ensure $data is either @null, array or object of class with configured mapping.
435
		$converter = new Converter();
436
437
		$data = $converter->convert($data);
438
		if ($data !== null && !is_object($data)) {
439
			// ensure we get object in final JSON structure in data node
440
			$data = (object)$data;
441
		}
442
443
		/** @noinspection PhpUndefinedClassInspection */
444
		$response = [
445
			static::KEY_SUCCESS => $success,
446
			static::KEY_CODE    => $api_code,
447
			static::KEY_LOCALE  => \App::getLocale(),
448
			static::KEY_MESSAGE => $message,
449
			static::KEY_DATA    => $data,
450
		];
451
452
		if ($debug_data !== null) {
453
			$debug_key = Config::get(static::CONF_KEY_DEBUG_DEBUG_KEY, self::KEY_DEBUG);
454
			$response[ $debug_key ] = $debug_data;
455
		}
456
457
		return $response;
458
	}
459
460
461
}
462