Passed
Pull Request — master (#204)
by Marcin
14:41 queued 04:41
created

ResponseBuilder   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 432
Duplicated Lines 0 %

Test Coverage

Coverage 98.21%

Importance

Changes 10
Bugs 0 Features 0
Metric Value
eloc 117
c 10
b 0
f 0
dl 0
loc 432
ccs 110
cts 112
cp 0.9821
rs 10
wmc 30

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A withDebugData() 0 7 1
A getMessageForApiCode() 0 17 4
A asSuccess() 0 4 1
A error() 0 9 1
A withData() 0 14 1
A withHttpHeaders() 0 5 1
B buildResponse() 0 38 7
A withMessage() 0 7 1
A success() 0 9 1
A asError() 0 14 3
A withJsonOptions() 0 7 1
A make() 0 19 3
A build() 0 26 2
A withHttpCode() 0 8 1
A withPlaceholders() 0 5 1
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