Passed
Pull Request — master (#204)
by Marcin
07:42
created

ResponseBuilder::make()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 10
c 1
b 0
f 0
nc 4
nop 9
dl 0
loc 19
ccs 11
cts 11
cp 1
crap 3
rs 9.9332

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-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 Illuminate\Support\Facades\Config;
18
use Illuminate\Support\Facades\Response;
19
use Symfony\Component\HttpFoundation\Response as HttpResponse;
20
use MarcinOrlowski\ResponseBuilder\ResponseBuilder as RB;
21
use MarcinOrlowski\ResponseBuilder\Exceptions as Ex;
22
23
/**
24
 * Builds standardized HttpResponse response object
25
 */
26
class ResponseBuilder extends ResponseBuilderBase
27
{
28
	/** @var bool */
29
	protected $success = false;
30
31
	/** @var int */
32
	protected $api_code;
33
34
	/** @var int|null */
35
	protected $http_code = null;
36
37
	/** @var mixed */
38
	protected $data = null;
39
40
	/** @var string|null */
41
	protected $message = null;
42
43
	/** @var array|null */
44
	protected $placeholders = null;
45
46
	/** @var int|null */
47
	protected $json_opts = null;
48
49
	/** @var array|null */
50
	protected $debug_data = null;
51
52
	/** @var array */
53
	protected $http_headers = [];
54
55
	// -----------------------------------------------------------------------------------------------------------
56
57
	/**
58
	 * Private constructor. Use asSuccess() and asError() static methods to obtain instance of Builder.
59
	 *
60
	 * @param bool $success
61
	 * @param int  $api_code
62 26
	 */
63
	protected function __construct(bool $success, int $api_code)
64 26
	{
65 26
		$this->success = $success;
66 26
		$this->api_code = $api_code;
67
	}
68
69
	// -----------------------------------------------------------------------------------------------------------
70
71
	/**
72
	 * Returns success
73
	 *
74
	 * @param mixed|null   $data          Array of primitives and supported objects to be returned in 'data' node
75
	 *                                    of the JSON response, single supported object or @null if there's no
76
	 *                                    to be returned.
77
	 * @param integer|null $api_code      API code to be returned or @null to use value of BaseApiCodes::OK().
78
	 * @param array|null   $placeholders  Placeholders passed to Lang::get() for message placeholders
79
	 *                                    substitution or @null if none.
80
	 * @param integer|null $http_code     HTTP code to be used for HttpResponse sent or @null
81
	 *                                    for default DEFAULT_HTTP_CODE_OK.
82
	 * @param integer|null $json_opts     See http://php.net/manual/en/function.json-encode.php for supported
83
	 *                                    options or pass @null to use value from your config (or defaults).
84
	 *
85
	 * @return HttpResponse
86 8
	 *
87
	 * @throws Ex\MissingConfigurationKeyException
88
	 * @throws Ex\ConfigurationNotFoundException
89 8
	 * @throws Ex\IncompatibleTypeException
90 8
	 * @throws Ex\ArrayWithMixedKeysException
91 6
	 * @throws Ex\InvalidTypeException
92 6
	 * @throws Ex\NotIntegerException
93 6
	 */
94 6
	public static function success($data = null, int $api_code = null, array $placeholders = null,
95
	                               int $http_code = null, int $json_opts = null): HttpResponse
96
	{
97
		return static::asSuccess($api_code)
98
			->withData($data)
99
			->withPlaceholders($placeholders)
100
			->withHttpCode($http_code)
101
			->withJsonOptions($json_opts)
102
			->build();
103
	}
104
105
	/**
106
	 * Builds error Response object. Supports optional arguments passed to Lang::get() if associated error
107
	 * message uses placeholders as well as return data payload
108
	 *
109
	 * @param integer           $api_code      Your API code to be returned with the response object.
110
	 * @param array|null        $placeholders  Placeholders passed to Lang::get() for message placeholders
111
	 *                                         substitution or @null if none.
112
	 * @param object|array|null $data          Array of primitives and supported objects to be returned in 'data' node
113
	 *                                         of the JSON response, single supported object or @null if there's no
114 1
	 *                                         to be returned.
115
	 * @param integer|null      $http_code     HTTP code to be used for HttpResponse sent or @null
116
	 *                                         for default DEFAULT_HTTP_CODE_ERROR.
117 1
	 * @param integer|null      $json_opts     See http://php.net/manual/en/function.json-encode.php for supported
118 1
	 *                                         options or pass @null to use value from your config (or defaults).
119 1
	 *
120 1
	 * @return HttpResponse
121 1
	 *
122 1
	 * @throws Ex\ArrayWithMixedKeysException
123
	 * @throws Ex\MissingConfigurationKeyException
124
	 * @throws Ex\ConfigurationNotFoundException
125
	 * @throws Ex\IncompatibleTypeException
126
	 * @throws Ex\InvalidTypeException
127
	 * @throws Ex\NotIntegerException
128
	 */
129
	public static function error(int $api_code, array $placeholders = null, $data = null, int $http_code = null,
130
	                             int $json_opts = null): HttpResponse
131
	{
132 16
		return static::asError($api_code)
133
			->withPlaceholders($placeholders)
134 16
			->withData($data)
135
			->withHttpCode($http_code)
136
			->withJsonOptions($json_opts)
137
			->build();
138
	}
139
140
	// -----------------------------------------------------------------------------------------------------------
141
142 11
	/**
143
	 * @param int|null $api_code
144 11
	 *
145 11
	 * @return \MarcinOrlowski\ResponseBuilder\ResponseBuilder
146 10
	 *
147
	 * @throws Ex\InvalidTypeException
148 11
	 * @throws Ex\MissingConfigurationKeyException
149 1
	 * @throws Ex\NotIntegerException
150 1
	 */
151
	public static function asSuccess(int $api_code = null): self
152
	{
153 10
		/** @noinspection PhpUnhandledExceptionInspection */
154
		return new static(true, $api_code ?? BaseApiCodes::OK());
155
	}
156
157
	/**
158
	 * @param int $api_code
159
	 *
160
	 * @return \MarcinOrlowski\ResponseBuilder\ResponseBuilder
161 16
	 *
162
	 * @throws Ex\MissingConfigurationKeyException
163 16
	 * @throws Ex\NotIntegerException
164 16
	 * @throws Ex\InvalidTypeException
165 16
	 */
166
	public static function asError(int $api_code): self
167 16
	{
168
		/** @noinspection PhpUnhandledExceptionInspection */
169
		$code_ok = BaseApiCodes::OK();
170
		if ($api_code !== $code_ok) {
171
			/** @noinspection PhpUnhandledExceptionInspection */
172
			Validator::assertIsIntRange('api_code', $api_code, BaseApiCodes::getMinCode(), BaseApiCodes::getMaxCode());
173
		}
174
		if ($api_code === $code_ok) {
175 18
			throw new \OutOfBoundsException(
176
				"Error response cannot use api_code of value {$code_ok} which is reserved for OK.");
177 18
		}
178 18
179 18
		return new static(false, $api_code);
180 16
	}
181
182 16
	/**
183
	 * @param int|null $http_code
184
	 *
185
	 * @return $this
186
	 *
187
	 * @throws Ex\InvalidTypeException
188
	 */
189
	public function withHttpCode(int $http_code = null): self
190 7
	{
191
		Validator::assertIsType('http_code', $http_code, [
192 7
			Type::INTEGER,
193 7
			Type::NULL]);
194 7
		$this->http_code = $http_code;
195
196 7
		return $this;
197
	}
198
199
	/**
200
	 * @param mixed $data
201
	 *
202
	 * @return $this
203
	 *
204 9
	 * @throws Ex\InvalidTypeException
205
	 */
206 9
	public function withData($data = null): self
207 9
	{
208 9
		Validator::assertIsType('data', $data, [
209
			Type::ARRAY,
210 9
			Type::BOOLEAN,
211
			Type::INTEGER,
212
			Type::NULL,
213
			Type::OBJECT,
214
			Type::STRING,
215
			Type::DOUBLE,
216
		]);
217
		$this->data = $data;
218 9
219
		return $this;
220 9
	}
221 9
222 9
	/**
223
	 * @param int|null $json_opts
224 9
	 *
225
	 * @return $this
226
	 *
227
	 * @throws Ex\InvalidTypeException
228
	 */
229
	public function withJsonOptions(int $json_opts = null): self
230
	{
231
		Validator::assertIsType('json_opts', $json_opts, [Type::INTEGER,
232 7
		                                                  Type::NULL]);
233
		$this->json_opts = $json_opts;
234 7
235
		return $this;
236 7
	}
237
238
	/**
239
	 * @param array|null $debug_data
240
	 *
241
	 * @return $this
242
	 *
243
	 * @throws Ex\InvalidTypeException
244 1
	 */
245
	public function withDebugData(array $debug_data = null): self
246 1
	{
247
		Validator::assertIsType('$debug_data', $debug_data, [Type::ARRAY,
248 1
		                                                     Type::NULL]);
249
		$this->debug_data = $debug_data;
250
251
		return $this;
252
	}
253
254
	/**
255
	 * @param string|null $msg
256
	 *
257
	 * @return $this
258 18
	 *
259
	 * @throws Ex\InvalidTypeException
260 18
	 */
261 18
	public function withMessage(string $msg = null): self
262
	{
263 18
		Validator::assertIsType('message', $msg, [Type::STRING,
264 18
		                                          Type::NULL]);
265
		$this->message = $msg;
266 18
267 8
		return $this;
268 8
	}
269
270 8
	/**
271
	 * @param array|null $placeholders
272 8
	 *
273 8
	 * @return $this
274
	 */
275 10
	public function withPlaceholders(array $placeholders = null): self
276
	{
277 10
		$this->placeholders = $placeholders;
278
279 10
		return $this;
280 10
	}
281
282
	/**
283 17
	 * @param array|null $http_headers
284
	 *
285
	 * @return $this
286
	 */
287
	public function withHttpHeaders(array $http_headers = null): self
288
	{
289
		$this->http_headers = $http_headers ?? [];
290
291
		return $this;
292
	}
293
294
	/**
295
	 * Builds and returns final HttpResponse. It's safe to call build() as many times as needed, as no
296
	 * internal state is changed. It's also safe to alter any parameter set previously and call build()
297
	 * again to get new response object that includes new changes.
298
	 *
299
	 * @return \Symfony\Component\HttpFoundation\Response
300
	 *
301
	 * @throws Ex\ArrayWithMixedKeysException
302
	 * @throws Ex\ConfigurationNotFoundException
303
	 * @throws Ex\IncompatibleTypeException
304
	 * @throws Ex\InvalidTypeException
305
	 * @throws Ex\MissingConfigurationKeyException
306
	 * @throws Ex\NotIntegerException
307 24
	 */
308
	public function build(): HttpResponse
309
	{
310
		$api_code = $this->api_code;
311 24
		Validator::assertIsInt('api_code', $api_code);
312 24
313 24
		$msg_or_api_code = $this->message ?? $api_code;
314
		$http_headers = $this->http_headers ?? [];
315 24
316
		if ($this->success) {
317 23
			$api_code = $api_code ?? BaseApiCodes::OK();
318 23
			$http_code = $this->http_code ?? RB::DEFAULT_HTTP_CODE_OK;
319 1
320
			Validator::assertOkHttpCode($http_code);
321
322 22
			$result = $this->make($this->success, $api_code, $msg_or_api_code, $this->data, $http_code,
323 22
				$this->placeholders, $http_headers, $this->json_opts);
324
		} else {
325
			$http_code = $this->http_code ?? RB::DEFAULT_HTTP_CODE_ERROR;
326
327
			Validator::assertErrorHttpCode($http_code);
328
329
			$result = $this->make(false, $api_code, $msg_or_api_code, $this->data, $http_code, $this->placeholders,
330
				$this->http_headers, $this->json_opts, $this->debug_data);
331
		}
332
333
		return $result;
334
	}
335
336
	/**
337
	 * @param boolean           $success         @true if response reports successful operation, @false otherwise.
338
	 * @param integer           $api_code        Your API code to be returned with the response object.
339
	 * @param string|integer    $msg_or_api_code message string or valid API code to get message for
340
	 * @param object|array|null $data            optional additional data to be included in response object
341
	 * @param integer|null      $http_code       HTTP code for the HttpResponse or @null for either DEFAULT_HTTP_CODE_OK
342
	 *                                           or DEFAULT_HTTP_CODE_ERROR depending on the $success.
343
	 * @param array|null        $placeholders    Placeholders passed to Lang::get() for message placeholders
344
	 *                                           substitution or @null if none.
345
	 * @param array|null        $http_headers    Optional HTTP headers to be returned in the response.
346
	 * @param integer|null      $json_opts       See http://php.net/manual/en/function.json-encode.php for supported
347 22
	 *                                           options or pass @null to use value from your config (or defaults).
348
	 * @param array|null        $debug_data      Optional debug data array to be added to returned JSON.
349
	 *
350
	 * @return HttpResponse
351
	 *
352 22
	 * @throws Ex\MissingConfigurationKeyException
353 21
	 * @throws Ex\ConfigurationNotFoundException
354
	 * @throws Ex\ArrayWithMixedKeysException
355 6
	 * @throws Ex\IncompatibleTypeException
356
	 * @throws Ex\InvalidTypeException
357
	 * @throws Ex\NotIntegerException
358
	 * @throws Ex\NotStringException
359 21
	 *
360 10
	 * @noinspection PhpTooManyParametersInspection
361
	 */
362 11
	protected function make(bool $success, int $api_code, $msg_or_api_code, $data = null,
363 9
	                        int $http_code = null, array $placeholders = null, array $http_headers = null,
364
	                        int $json_opts = null, array $debug_data = null): HttpResponse
365
	{
366
		$http_headers = $http_headers ?? [];
367
		$http_code = $http_code ?? ($success ? RB::DEFAULT_HTTP_CODE_OK : RB::DEFAULT_HTTP_CODE_ERROR);
368 19
		$json_opts = $json_opts ?? Config::get(RB::CONF_KEY_ENCODING_OPTIONS, RB::DEFAULT_ENCODING_OPTIONS);
369 19
370 19
		Validator::assertIsInt('encoding_options', $json_opts);
371 19
372 19
		Validator::assertIsInt('api_code', $api_code);
373
		if (!BaseApiCodes::isCodeValid($api_code)) {
374
			/** @noinspection PhpUnhandledExceptionInspection */
375 19
			Validator::assertIsIntRange('api_code', $api_code, BaseApiCodes::getMinCode(), BaseApiCodes::getMaxCode());
376 2
		}
377 2
378
		return Response::json(
379
			$this->buildResponse($success, $api_code, $msg_or_api_code, $placeholders, $data, $debug_data),
380 19
			$http_code, $http_headers, $json_opts);
381
	}
382
383
	/**
384
	 * Creates standardised API response array. This is final method called in the whole pipeline before we
385
	 * return final JSON back to client. If you want to manipulate your response, this is the place to do that.
386
	 * If you set APP_DEBUG to true, 'code_hex' field will be additionally added to reported JSON for easier
387
	 * manual debugging.
388
	 *
389
	 * @param boolean           $success         @true if response reports successful operation, @false otherwise.
390
	 * @param integer           $api_code        Your API code to be returned with the response object.
391
	 * @param string|integer    $msg_or_api_code Message string or valid API code to get message for.
392
	 * @param array|null        $placeholders    Placeholders passed to Lang::get() for message placeholders
393
	 *                                           substitution or @null if none.
394
	 * @param object|array|null $data            API response data if any
395 10
	 * @param array|null        $debug_data      optional debug data array to be added to returned JSON.
396
	 *
397
	 * @return array response ready to be encoded as json and sent back to client
398
	 *
399 10
	 * @throws Ex\ArrayWithMixedKeysException
400 10
	 * @throws Ex\ConfigurationNotFoundException
401
	 * @throws Ex\IncompatibleTypeException
402
	 * @throws Ex\MissingConfigurationKeyException
403
	 * @throws Ex\InvalidTypeException
404
	 *
405
	 * @noinspection PhpTooManyParametersInspection
406 10
	 */
407 10
	protected function buildResponse(bool $success, int $api_code,
408 10
	                                 $msg_or_api_code, array $placeholders = null,
409
	                                 $data = null, array $debug_data = null): array
410
	{
411 10
		// ensure $data is either @null, array or object of class with configured mapping.
412
		$data = (new Converter())->convert($data);
413
		if ($data !== null) {
414
			// ensure we get object in final JSON structure in data node
415
			$data = (object)$data;
416
		}
417
418
		if ($data === null && Config::get(RB::CONF_KEY_DATA_ALWAYS_OBJECT, false)) {
419
			$data = (object)[];
420
		}
421
422
		// get human readable message for API code or use message string (if given instead of API code)
423
		if (\is_int($msg_or_api_code)) {
424
			$message = $this->getMessageForApiCode($success, $msg_or_api_code, $placeholders);
425
		} else {
426
			Validator::assertIsType('message', $msg_or_api_code, [Type::STRING, Type::INTEGER]);
427
			$message = $msg_or_api_code;
428
		}
429
430
		/** @noinspection PhpUndefinedClassInspection */
431
		$response = [
432
			RB::KEY_SUCCESS => $success,
433
			RB::KEY_CODE    => $api_code,
434
			RB::KEY_LOCALE  => \App::getLocale(),
435
			RB::KEY_MESSAGE => $message,
436
			RB::KEY_DATA    => $data,
437
		];
438
439
		if ($debug_data !== null) {
440
			$debug_key = Config::get(RB::CONF_KEY_DEBUG_DEBUG_KEY, RB::KEY_DEBUG);
441
			$response[ $debug_key ] = $debug_data;
442
		}
443
444
		return $response;
445
	}
446
447
	/**
448
	 * If $msg_or_api_code is integer value, returns human readable message associated with that code (with
449
	 * fallback to built-in default string if no api code mapping is set. If $msg_or_api_code is a string,
450
	 * returns it unaltered.
451
	 *
452
	 * @param boolean    $success      @true if response reports successful operation, @false otherwise.
453
	 * @param integer    $api_code     Your API code to be returned with the response object.
454
	 * @param array|null $placeholders Placeholders passed to Lang::get() for message placeholders
455
	 *                                 substitution or @null if none.
456
	 *
457
	 * @return string
458
	 *
459
	 * @throws Ex\IncompatibleTypeException
460
	 * @throws Ex\MissingConfigurationKeyException
461
	 * @throws Ex\InvalidTypeException
462
	 * @throws Ex\NotIntegerException
463
	 */
464
	protected function getMessageForApiCode(bool $success, int $api_code, array $placeholders = null): string
465
	{
466
		// We got integer value here not a message string, so we need to check if we have the mapping for
467
		// this string already configured.
468
		$key = BaseApiCodes::getCodeMessageKey($api_code);
469
		if ($key === null) {
470
			// nope, let's get the default one instead, based of
471
			$fallback_code = $success ? BaseApiCodes::OK() : BaseApiCodes::NO_ERROR_MESSAGE();
472
			// default messages are expected to be always available
473
			/** @var string $key */
474
			$key = BaseApiCodes::getCodeMessageKey($fallback_code);
475
		}
476
477
		$placeholders = $placeholders ?? [];
478
		if (!\array_key_exists('api_code', $placeholders)) {
479
			$placeholders['api_code'] = $api_code;
480
		}
481
482
		$msg = \Lang::get($key, $placeholders);
483
484
		// As Lang::get() is documented to also returning arrays(?)...
485
		if (is_array($msg)) {
486
			$msg = implode('', $msg);
487
		}
488
489
		return $msg;
490
	}
491
}
492