Completed
Pull Request — master (#90)
by Marcin
05:35 queued 02:13
created

ResponseBuilder::make()   B

Complexity

Conditions 8
Paths 10

Size

Total Lines 40
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 16
Bugs 0 Features 0
Metric Value
cc 8
eloc 22
c 16
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
	 * Checks if we have "classes" mapping configured for $data object class.
128
	 * Returns @true if there's valid config for this class.
129
	 *
130
	 * @param object $data Object to check mapping for.
131
	 *
132
	 * @return bool
133
	 *
134
	 * @throws \InvalidArgumentException if $data is not an object.
135
	 */
136
	protected static function hasClassesMapping(object $data): bool
137
	{
138
		return array_key_exists(get_class($data), static::getClassesMapping());
139
	}
140
141
	/**
142
	 * Recursively walks $data array and converts all known objects if found. Note
143
	 * $data array is passed by reference so source $data array may be modified.
144
	 *
145
	 * @param array $classes "classes" config mapping array
146
	 * @param array $data    array to recursively convert known elements of
147
	 *
148
	 * @return void
149
	 */
150
	protected static function convert(array $classes, array &$data): void
151
	{
152
		foreach ($data as $data_key => &$data_val) {
153
			if (is_array($data_val)) {
154
				static::convert($classes, $data_val);
155
			} elseif (is_object($data_val)) {
156
				$obj_class_name = get_class($data_val);
157
				if (array_key_exists($obj_class_name, $classes)) {
158
					$conversion_method = $classes[ $obj_class_name ][ static::KEY_METHOD ];
159
					$converted = $data_val->$conversion_method();
160
					$data[ $data_key ] = $converted;
161
				}
162
			}
163
		}
164
	}
165
166
	/**
167
	 * Returns success
168
	 *
169
	 * @param object|array|null $data             payload to be returned as 'data' node, @null if none
170
	 * @param integer|null      $api_code         API code to be returned with the response or @null for default `OK` code
171
	 * @param array|null        $lang_args        arguments passed to Lang if message associated with API code uses placeholders
172
	 * @param integer|null      $http_code        HTTP return code to be set for this response or @null for default (200)
173
	 * @param integer|null      $encoding_options see http://php.net/manual/en/function.json-encode.php or @null to use
174
	 *                                            config's value or defaults
175
	 *
176
	 * @return HttpResponse
177
	 */
178
	public static function success($data = null, $api_code = null, array $lang_args = null,
179
	                               int $http_code = null, int $encoding_options = null): HttpResponse
180
	{
181
		return static::buildSuccessResponse($data, $api_code, $lang_args, $http_code, $encoding_options);
182
	}
183
184
	/**
185
	 * Returns success
186
	 *
187
	 * @param integer|null $api_code  API code to be returned with the response or @null for default `OK` code
188
	 * @param array|null   $lang_args arguments passed to Lang if message associated with API code uses placeholders
189
	 * @param integer|null $http_code HTTP return code to be set for this response or @null for default (200)
190
	 *
191
	 * @return HttpResponse
192
	 */
193
	public static function successWithCode(int $api_code = null, array $lang_args = null, int $http_code = null): HttpResponse
194
	{
195
		return static::success(null, $api_code, $lang_args, $http_code);
196
	}
197
198
	/**
199
	 * Returns success with custom HTTP code
200
	 *
201
	 * @param integer|null $http_code HTTP return code to be set for this response. If @null is passed, falls back
202
	 *                                to DEFAULT_HTTP_CODE_OK.
203
	 *
204
	 * @return HttpResponse
205
	 */
206
	public static function successWithHttpCode(int $http_code = null): HttpResponse
207
	{
208
		return static::buildSuccessResponse(null, BaseApiCodes::OK(), [], $http_code);
209
	}
210
211
	/**
212
	 * @param object|array|null $data             payload to be returned as 'data' node, @null if none
213
	 * @param integer|null      $api_code         API code to be returned with the response or @null for `OK` code
214
	 * @param array|null        $lang_args        arguments passed to Lang if message associated with API code uses placeholders
215
	 * @param integer|null      $http_code        HTTP return code to be set for this response
216
	 * @param integer|null      $encoding_options see http://php.net/manual/en/function.json-encode.php or @null to use
217
	 *                                            config's value or defaults
218
	 *
219
	 * @return HttpResponse
220
	 *
221
	 * @throws \InvalidArgumentException Thrown when provided arguments are invalid.
222
	 */
223
	protected static function buildSuccessResponse($data = null, int $api_code = null, array $lang_args = null,
224
	                                               int $http_code = null, int $encoding_options = null): HttpResponse
225
	{
226
		$http_code = $http_code ?? static::DEFAULT_HTTP_CODE_OK;
227
		$api_code = $api_code ?? BaseApiCodes::OK();
228
229
		Validator::assertInt('api_code', $api_code);
230
		Validator::assertInt('http_code', $http_code);
231
		Validator::assertIntRange('http_code', $http_code, 200, 299);
232
233
		return static::make(true, $api_code, $api_code, $data, $http_code, $lang_args, null, $encoding_options);
234
	}
235
236
	/**
237
	 * Builds error Response object. Supports optional arguments passed to Lang::get() if associated error
238
	 * message uses placeholders as well as return data payload
239
	 *
240
	 * @param integer           $api_code         API code to be returned with the response
241
	 * @param array|null        $lang_args        arguments array passed to Lang::get() for messages with placeholders
242
	 * @param object|array|null $data             payload array to be returned in 'data' node or response object
243
	 * @param integer|null      $http_code        optional HTTP status code to be used with this response or @null for default
244
	 * @param integer|null      $encoding_options see http://php.net/manual/en/function.json-encode.php or @null to use
245
	 *                                            config's value or defaults
246
	 *
247
	 * @return HttpResponse
248
	 */
249
	public static function error(int $api_code, array $lang_args = null, $data = null, int $http_code = null,
250
	                             int $encoding_options = null): HttpResponse
251
	{
252
		return static::buildErrorResponse($data, $api_code, $http_code, $lang_args, $encoding_options);
253
	}
254
255
	/**
256
	 * @param integer           $api_code         API code to be returned with the response
257
	 * @param object|array|null $data             payload to be returned as 'data' node, @null if none
258
	 * @param array|null        $lang_args        arguments array passed to Lang::get() for messages with placeholders
259
	 * @param integer|null      $encoding_options see http://php.net/manual/en/function.json-encode.php or @null to use
260
	 *                                            config's value or defaults
261
	 *
262
	 * @return HttpResponse
263
	 */
264
	public static function errorWithData(int $api_code, $data, array $lang_args = null,
265
	                                     int $encoding_options = null): HttpResponse
266
	{
267
		return static::buildErrorResponse($data, $api_code, null, $lang_args, $encoding_options);
268
	}
269
270
	/**
271
	 * @param integer           $api_code         API code to be returned with the response
272
	 * @param object|array|null $data             payload to be returned as 'data' node, @null if none
273
	 * @param integer|null      $http_code        HTTP error code to be returned with this Cannot be @null
274
	 * @param array|null        $lang_args        arguments array passed to Lang::get() for messages with placeholders
275
	 * @param integer|null      $encoding_options see http://php.net/manual/en/function.json-encode.php or @null to use
276
	 *                                            config's value or defaults
277
	 *
278
	 * @return HttpResponse
279
	 *
280
	 * @throws \InvalidArgumentException if http_code is @null
281
	 */
282
	public static function errorWithDataAndHttpCode(int $api_code, $data, int $http_code, array $lang_args = null,
283
	                                                int $encoding_options = null): HttpResponse
284
	{
285
		return static::buildErrorResponse($data, $api_code, $http_code, $lang_args, $encoding_options);
286
	}
287
288
	/**
289
	 * @param integer      $api_code  API code to be returned with the response
290
	 * @param integer|null $http_code HTTP return code to be set for this response or @null for default
291
	 * @param array|null   $lang_args arguments array passed to Lang::get() for messages with placeholders
292
	 *
293
	 * @return HttpResponse
294
	 *
295
	 * @throws \InvalidArgumentException if http_code is @null
296
	 */
297
	public static function errorWithHttpCode(int $api_code, int $http_code, array $lang_args = null): HttpResponse
298
	{
299
		return static::buildErrorResponse(null, $api_code, $http_code, $lang_args);
300
	}
301
302
	/**
303
	 * @param integer           $api_code         API code to be returned with the response
304
	 * @param string            $error_message    custom message to be returned as part of error response
305
	 * @param object|array|null $data             payload to be returned as 'data' node, @null if none
306
	 * @param integer|null      $http_code        optional HTTP status code to be used with this response or @null for defaults
307
	 * @param integer|null      $encoding_options see http://php.net/manual/en/function.json-encode.php or @null to use config's
308
	 *                                            value or defaults
309
	 *
310
	 * @return HttpResponse
311
	 */
312
	public static function errorWithMessageAndData(int $api_code, string $error_message, $data,
313
	                                               int $http_code = null, int $encoding_options = null): HttpResponse
314
	{
315
		return static::buildErrorResponse($data, $api_code, $http_code, null,
316
			$error_message, null, $encoding_options);
317
	}
318
319
	/**
320
	 * @param integer           $api_code         API code to be returned with the response
321
	 * @param string            $error_message    custom message to be returned as part of error response
322
	 * @param object|array|null $data             payload to be returned as 'data' node, @null if none
323
	 * @param integer|null      $http_code        optional HTTP status code to be used with this response or @null for defaults
324
	 * @param integer|null      $encoding_options see http://php.net/manual/en/function.json-encode.php or @null to use
325
	 *                                            config's value or defaults
326
	 * @param array|null        $debug_data       optional debug data array to be added to returned JSON.
327
	 *
328
	 * @return HttpResponse
329
	 */
330
	public static function errorWithMessageAndDataAndDebug(int $api_code, string $error_message, $data,
331
	                                                       int $http_code = null, int $encoding_options = null,
332
	                                                       array $debug_data = null): HttpResponse
333
	{
334
		return static::buildErrorResponse($data, $api_code, $http_code, null,
335
			$error_message, null, $encoding_options, $debug_data);
336
	}
337
338
	/**
339
	 * @param integer      $api_code      API code to be returned with the response
340
	 * @param string       $error_message custom message to be returned as part of error response
341
	 * @param integer|null $http_code     optional HTTP status code to be used with this response or @null for defaults
342
	 *
343
	 * @return HttpResponse
344
	 */
345
	public static function errorWithMessage(int $api_code, string $error_message, int $http_code = null): HttpResponse
346
	{
347
		return static::buildErrorResponse(null, $api_code, $http_code, null, $error_message);
348
	}
349
350
	/**
351
	 * Builds error Response object. Supports optional arguments passed to Lang::get() if associated error message
352
	 * uses placeholders as well as return data payload
353
	 *
354
	 * @param object|array|null $data             payload array to be returned in 'data' node or response object or @null if none
355
	 * @param integer           $api_code         API code to be returned with the response
356
	 * @param integer|null      $http_code        optional HTTP status code to be used with this response or @null for default
357
	 * @param array|null        $lang_args        arguments array passed to Lang::get() for messages with placeholders
358
	 * @param string|null       $message          custom message to be returned as part of error response
359
	 * @param array|null        $headers          optional HTTP headers to be returned in error response
360
	 * @param integer|null      $encoding_options see see json_encode() docs for valid option values. Use @null to fall back to
361
	 *                                            config's value or defaults
362
	 * @param array|null        $debug_data       optional debug data array to be added to returned JSON.
363
	 *
364
	 * @return HttpResponse
365
	 *
366
	 * @throws \InvalidArgumentException Thrown if $code is not correct, outside the range, equals OK code etc.
367
	 *
368
	 * @noinspection MoreThanThreeArgumentsInspection
369
	 */
370
	protected static function buildErrorResponse($data, int $api_code, int $http_code = null, array $lang_args = null,
371
	                                             string $message = null, array $headers = null, int $encoding_options = null,
372
	                                             array $debug_data = null): HttpResponse
373
	{
374
		$http_code = $http_code ?? static::DEFAULT_HTTP_CODE_ERROR;
375
		$headers = $headers ?? [];
376
377
		$code_ok = BaseApiCodes::OK();
378
379
		Validator::assertInt('api_code', $api_code);
380
		if ($api_code !== $code_ok) {
381
			Validator::assertIntRange('api_code', $api_code, BaseApiCodes::getMinCode(), BaseApiCodes::getMaxCode());
382
		}
383
		if ($api_code === $code_ok) {
384
			throw new \InvalidArgumentException("Error response cannot use api_code of value  {$code_ok} which is reserved for OK");
385
		}
386
387
		Validator::assertInt('http_code', $http_code);
388
		Validator::assertIntRange('http_code', $http_code, static::ERROR_HTTP_CODE_MIN, static::ERROR_HTTP_CODE_MAX);
389
390
		$message_or_api_code = $message ?? $api_code;
391
392
		return static::make(false, $api_code, $message_or_api_code, $data, $http_code,
393
			$lang_args, $headers, $encoding_options, $debug_data);
394
	}
395
396
	/**
397
	 * @param boolean           $success             @true if response indicate success, @false otherwise
398
	 * @param integer           $api_code            API code to be returned with the response
399
	 * @param string|integer    $message_or_api_code message string or valid API code
400
	 * @param object|array|null $data                optional additional data to be included in response object
401
	 * @param integer|null      $http_code           return HTTP code for build Response object
402
	 * @param array|null        $lang_args           arguments array passed to Lang::get() for messages with placeholders
403
	 * @param array|null        $headers             optional HTTP headers to be returned in the response
404
	 * @param integer|null      $encoding_options    see http://php.net/manual/en/function.json-encode.php
405
	 * @param array|null        $debug_data          optional debug data array to be added to returned JSON.
406
	 *
407
	 * @return HttpResponse
408
	 *
409
	 * @throws \InvalidArgumentException If $api_code is neither a string nor valid integer code.
410
	 * @throws \InvalidArgumentException if $data is an object of class that is not configured in "classes" mapping.
411
	 *
412
	 * @noinspection MoreThanThreeArgumentsInspection
413
	 */
414
	protected static function make(bool $success, int $api_code, $message_or_api_code, $data = null,
415
	                               int $http_code = null, array $lang_args = null, array $headers = null,
416
	                               int $encoding_options = null, array $debug_data = null): HttpResponse
417
	{
418
		$headers = $headers ?? [];
419
		$http_code = $http_code ?? ($success ? static::DEFAULT_HTTP_CODE_OK : static::DEFAULT_HTTP_CODE_ERROR);
420
		$encoding_options = $encoding_options ?? Config::get(self::CONF_KEY_ENCODING_OPTIONS, static::DEFAULT_ENCODING_OPTIONS);
421
422
		Validator::assertInt('encoding_options', $encoding_options);
423
424
		Validator::assertInt('api_code', $api_code);
425
		if (!BaseApiCodes::isCodeValid($api_code)) {
426
			$min = BaseApiCodes::getMinCode();
427
			$max = BaseApiCodes::getMaxCode();
428
			throw new \InvalidArgumentException("API code value ({$api_code}) is out of allowed range {$min}-{$max}");
429
		}
430
431
		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...
432
			throw new \InvalidArgumentException(
433
				sprintf('Message must be either string or resolvable integer API code (%s given)',
434
					gettype($message_or_api_code))
435
			);
436
		}
437
438
		// we got code, not message string, so we need to check if we have the mapping for
439
		// this string already configured.
440
		if (is_int($message_or_api_code)) {
441
			$key = BaseApiCodes::getCodeMessageKey($message_or_api_code);
442
			if ($key === null) {
443
				// nope, let's get the default one instead
444
				$key = BaseApiCodes::getCodeMessageKey($success ? BaseApiCodes::OK() : BaseApiCodes::NO_ERROR_MESSAGE());
445
			}
446
447
			$lang_args = $lang_args ?? ['api_code' => $message_or_api_code];
448
			$message_or_api_code = \Lang::get($key, $lang_args);
449
		}
450
451
		return Response::json(
452
			static::buildResponse($success, $api_code, $message_or_api_code, $data, $debug_data),
453
			$http_code, $headers, $encoding_options
454
		);
455
	}
456
457
	/**
458
	 * Creates standardised API response array. If you set APP_DEBUG to true, 'code_hex' field will be
459
	 * additionally added to reported JSON for easier manual debugging.
460
	 *
461
	 * @param boolean           $success    @true if response indicates success, @false otherwise
462
	 * @param integer           $api_code   response code
463
	 * @param string            $message    message to return
464
	 * @param object|array|null $data       API response data if any
465
	 * @param array|null        $debug_data optional debug data array to be added to returned JSON.
466
	 *
467
	 * @return array response ready to be encoded as json and sent back to client
468
	 *
469
	 * @throws \RuntimeException in case of missing or invalid "classes" mapping configuration
470
	 */
471
	protected static function buildResponse(bool $success, int $api_code, string $message, $data = null,
472
	                                        array $debug_data = null): array
473
	{
474
		// ensure $data is either @null, array or object of class with configured mapping.
475
		if ($data !== null) {
476
			if (!is_array($data) && !is_object($data)) {
477
				throw new \InvalidArgumentException(
478
					sprintf('Invalid payload data. Must be null, array or class with mapping ("%s" given).', gettype($data)));
479
			}
480
481
			if (is_object($data) && !static::hasClassesMapping($data)) {
482
				throw new \InvalidArgumentException(sprintf('No mapping configured for "%s" class.', get_class($data)));
483
			}
484
485
			// Preliminary validation passed. Let's walk and convert...
486
			// we can do some auto-conversion on known class types, so check for that first
487
			/** @var array $classes */
488
			$classes = static::getClassesMapping();
489
			if (($classes !== null) && (count($classes) > 0)) {
490
				if (is_array($data)) {
491
					static::convert($classes, $data);
492
				} elseif (is_object($data)) {
493
					$obj_class_name = get_class($data);
494
					if (array_key_exists($obj_class_name, $classes)) {
495
						$conversion_method = $classes[ $obj_class_name ][ static::KEY_METHOD ];
496
						$data = [$classes[ $obj_class_name ][ static::KEY_KEY ] => $data->$conversion_method()];
497
					}
498
				}
499
			}
500
		}
501
502
		if ($data !== null && !is_object($data)) {
503
			// ensure we get object in final JSON structure in data node
504
			$data = (object)$data;
505
		}
506
507
		/** @noinspection PhpUndefinedClassInspection */
508
		$response = [
509
			static::KEY_SUCCESS => $success,
510
			static::KEY_CODE    => $api_code,
511
			static::KEY_LOCALE  => \App::getLocale(),
512
			static::KEY_MESSAGE => $message,
513
			static::KEY_DATA    => $data,
514
		];
515
516
		if ($debug_data !== null) {
517
			$debug_key = Config::get(static::CONF_KEY_DEBUG_DEBUG_KEY, self::KEY_DEBUG);
518
			$response[ $debug_key ] = $debug_data;
519
		}
520
521
		return $response;
522
	}
523
524
525
}
526