Passed
Pull Request — master (#204)
by Marcin
13:03 queued 02:49
created

ResponseBuilder::make()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

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