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

ResponseBuilder::hasClassesMapping()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
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