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

ResponseBuilder::buildResponse()   B

Complexity

Conditions 6
Paths 16

Size

Total Lines 38
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 6

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 6
eloc 20
c 3
b 0
f 0
nc 16
nop 6
dl 0
loc 38
ccs 3
cts 3
cp 1
crap 6
rs 8.9777
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