Completed
Push — master ( c6554e...bc1f52 )
by Marcin
16s queued 11s
created

ResponseBuilder   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 387
Duplicated Lines 0 %

Test Coverage

Coverage 98.21%

Importance

Changes 24
Bugs 0 Features 0
Metric Value
eloc 109
c 24
b 0
f 0
dl 0
loc 387
ccs 110
cts 112
cp 0.9821
rs 10
wmc 28

16 Methods

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