ResponseBuilder   A
last analyzed

Complexity

Total Complexity 26

Size/Duplication

Total Lines 393
Duplicated Lines 0 %

Test Coverage

Coverage 98.21%

Importance

Changes 12
Bugs 0 Features 0
Metric Value
eloc 112
dl 0
loc 393
ccs 110
cts 112
cp 0.9821
rs 10
c 12
b 0
f 0
wmc 26

16 Methods

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