Skip to content

Commit 79c79e2

Browse files
vinterskogenthilanga
authored andcommitted
JSON optional formatting feature (dingo#1354)
* Added JSON optional formatting feature * StyleCI fixes * StyleCI quotes fix * Underscores in varibales to camelcase * Grammar and styling improvements * Made setOptions method fluent * Fixed return in docblock * Allow mixed content to encode to json * Same for jsonp
1 parent 2b45b20 commit 79c79e2

17 files changed

+658
-9
lines changed

config/api.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@
208208
|
209209
| Responses can be returned in multiple formats by registering different
210210
| response formatters. You can also customize an existing response
211-
| formatter.
211+
| formatter with a number of options to configure its output.
212212
|
213213
*/
214214

@@ -220,4 +220,14 @@
220220

221221
],
222222

223+
'formatsOptions' => [
224+
225+
'json' => [
226+
'pretty_print' => env('API_JSON_FORMAT_PRETTY_PRINT_ENABLED', false),
227+
'indent_style' => env('API_JSON_FORMAT_INDENT_STYLE', 'space'),
228+
'indent_size' => env('API_JSON_FORMAT_INDENT_SIZE', 2),
229+
],
230+
231+
],
232+
223233
];

src/Http/Response.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ class Response extends IlluminateResponse
3939
*/
4040
protected static $formatters = [];
4141

42+
/**
43+
* Array of formats' options.
44+
*
45+
* @var array
46+
*/
47+
protected static $formatsOptions = [];
48+
4249
/**
4350
* Transformer factory instance.
4451
*
@@ -130,6 +137,8 @@ public function morph($format = 'json')
130137

131138
$formatter = static::getFormatter($format);
132139

140+
$formatter->setOptions(static::getFormatsOptions($format));
141+
133142
$defaultContentType = $this->headers->get('Content-Type');
134143

135144
$this->headers->set('Content-Type', $formatter->getContentType());
@@ -249,6 +258,46 @@ public static function setFormatters(array $formatters)
249258
static::$formatters = $formatters;
250259
}
251260

261+
/**
262+
* Set the formats' options.
263+
*
264+
* @param array $formatsOptions
265+
*
266+
* @return void
267+
*/
268+
public static function setFormatsOptions(array $formatsOptions)
269+
{
270+
static::$formatsOptions = $formatsOptions;
271+
}
272+
273+
/**
274+
* Get the format's options.
275+
*
276+
* @param string $format
277+
*
278+
* @return array
279+
*/
280+
public static function getFormatsOptions($format)
281+
{
282+
if (! static::hasOptionsForFormat($format)) {
283+
return [];
284+
}
285+
286+
return static::$formatsOptions[$format];
287+
}
288+
289+
/**
290+
* Determine if any format's options were set.
291+
*
292+
* @param string $format
293+
*
294+
* @return bool
295+
*/
296+
public static function hasOptionsForFormat($format)
297+
{
298+
return isset(static::$formatsOptions[$format]);
299+
}
300+
252301
/**
253302
* Add a response formatter.
254303
*

src/Http/Response/Format/Format.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ abstract class Format
1818
*/
1919
protected $response;
2020

21+
/*
22+
* Array of formats' options.
23+
*
24+
* @var array
25+
*/
26+
protected $options;
27+
2128
/**
2229
* Set the request instance.
2330
*
@@ -46,6 +53,20 @@ public function setResponse($response)
4653
return $this;
4754
}
4855

56+
/**
57+
* Set the formats' options.
58+
*
59+
* @param array $options
60+
*
61+
* @return \Dingo\Api\Http\Response\Format\Format
62+
*/
63+
public function setOptions(array $options)
64+
{
65+
$this->options = $options;
66+
67+
return $this;
68+
}
69+
4970
/**
5071
* Format an Eloquent model.
5172
*

src/Http/Response/Format/Json.php

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@
77

88
class Json extends Format
99
{
10+
/*
11+
* JSON format (as well as JSONP) uses JsonOptionalFormatting trait, which
12+
* provides extra functionality for the process of encoding data to
13+
* its JSON representation.
14+
*/
15+
use JsonOptionalFormatting;
16+
1017
/**
1118
* Format an Eloquent model.
1219
*
@@ -91,15 +98,29 @@ protected function morphToArray($value)
9198
/**
9299
* Encode the content to its JSON representation.
93100
*
94-
* @param string $content
101+
* @param mixed $content
95102
*
96103
* @return string
97104
*/
98105
protected function encode($content)
99106
{
100-
$encodedString = json_encode($content);
101-
if ($encodedString === false) {
102-
throw new \ErrorException('Error encoding data in JSON format: '.json_last_error());
107+
$jsonEncodeOptions = [];
108+
109+
// Here is a place, where any available JSON encoding options, that
110+
// deal with users' requirements to JSON response formatting and
111+
// structure, can be conveniently applied to tweak the output.
112+
113+
if ($this->isJsonPrettyPrintEnabled()) {
114+
$jsonEncodeOptions[] = JSON_PRETTY_PRINT;
115+
}
116+
117+
$encodedString = $this->performJsonEncoding($content, $jsonEncodeOptions);
118+
119+
if ($this->isCustomIndentStyleRequired()) {
120+
$encodedString = $this->indentPrettyPrintedJson(
121+
$encodedString,
122+
$this->options['indent_style']
123+
);
103124
}
104125

105126
return $encodedString;
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
<?php
2+
3+
namespace Dingo\Api\Http\Response\Format;
4+
5+
trait JsonOptionalFormatting
6+
{
7+
/*
8+
* Supported JSON pretty print indent styles.
9+
*
10+
* @var array
11+
*/
12+
protected $indentStyles = [
13+
'tab',
14+
'space',
15+
];
16+
17+
/*
18+
* Indent styles, that are allowed to have various indent size.
19+
*
20+
* @var array
21+
*/
22+
protected $hasVariousIndentSize = [
23+
'space',
24+
];
25+
26+
/*
27+
* Indent chars, associated with indent styles.
28+
*
29+
* @var array
30+
*/
31+
protected $indentChars = [
32+
'tab' => "\t",
33+
'space' => ' ',
34+
];
35+
36+
/*
37+
* JSON constants, that are allowed to be used as options while encoding.
38+
* Whitelist can be extended by other options in the future.
39+
*
40+
* @see http://php.net/manual/ru/json.constants.php
41+
*
42+
* @var array
43+
*/
44+
protected $jsonEncodeOptionsWhitelist = [
45+
JSON_PRETTY_PRINT,
46+
];
47+
48+
/**
49+
* Determine if JSON pretty print option is set to true.
50+
*
51+
* @return bool
52+
*/
53+
protected function isJsonPrettyPrintEnabled()
54+
{
55+
return isset($this->options['pretty_print']) && $this->options['pretty_print'] === true;
56+
}
57+
58+
/**
59+
* Determine if JSON custom indent style is set.
60+
*
61+
* @return bool
62+
*/
63+
protected function isCustomIndentStyleRequired()
64+
{
65+
return $this->isJsonPrettyPrintEnabled() &&
66+
isset($this->options['indent_style']) &&
67+
in_array($this->options['indent_style'], $this->indentStyles);
68+
}
69+
70+
/**
71+
* Perform JSON encode.
72+
*
73+
* @param string $content
74+
* @param array $jsonEncodeOptions
75+
*
76+
* @return string
77+
*/
78+
protected function performJsonEncoding($content, array $jsonEncodeOptions = [])
79+
{
80+
$jsonEncodeOptions = $this->filterJsonEncodeOptions($jsonEncodeOptions);
81+
82+
$optionsBitmask = $this->calucateJsonEncodeOptionsBitmask($jsonEncodeOptions);
83+
84+
if (($encodedString = json_encode($content, $optionsBitmask)) === false) {
85+
throw new \ErrorException('Error encoding data in JSON format: '.json_last_error());
86+
}
87+
88+
return $encodedString;
89+
}
90+
91+
/**
92+
* Filter JSON encode options array against the whitelist array.
93+
*
94+
* @param array $jsonEncodeOptions
95+
*
96+
* @return array
97+
*/
98+
protected function filterJsonEncodeOptions(array $jsonEncodeOptions)
99+
{
100+
return array_intersect($jsonEncodeOptions, $this->jsonEncodeOptionsWhitelist);
101+
}
102+
103+
/**
104+
* Sweep JSON encode options together to get options' bitmask.
105+
*
106+
* @param array $jsonEncodeOptions
107+
*
108+
* @return int
109+
*/
110+
protected function calucateJsonEncodeOptionsBitmask(array $jsonEncodeOptions)
111+
{
112+
return array_sum($jsonEncodeOptions);
113+
}
114+
115+
/**
116+
* Indent pretty printed JSON string, using given indent style.
117+
*
118+
* @param string $jsonString
119+
* @param string $indentStyle
120+
* @param int $defaultIndentSize
121+
*
122+
* @return string
123+
*/
124+
protected function indentPrettyPrintedJson($jsonString, $indentStyle, $defaultIndentSize = 2)
125+
{
126+
$indentChar = $this->getIndentCharForIndentStyle($indentStyle);
127+
$indentSize = $this->getPrettyPrintIndentSize() ?: $defaultIndentSize;
128+
129+
// If the given indentation style is allowed to have various indent size
130+
// (number of chars, that are used to indent one level in each line),
131+
// indent the JSON string with given (or default) indent size.
132+
if ($this->hasVariousIndentSize($indentStyle)) {
133+
return $this->peformIndentation($jsonString, $indentChar, $indentSize);
134+
}
135+
136+
// Otherwise following the convention, that indent styles, that does not
137+
// allowed to have various indent size (e.g. tab) are indented using
138+
// one tabulation character per one indent level in each line.
139+
return $this->peformIndentation($jsonString, $indentChar);
140+
}
141+
142+
/**
143+
* Get indent char for given indent style.
144+
*
145+
* @param string $indentStyle
146+
*
147+
* @return string
148+
*/
149+
protected function getIndentCharForIndentStyle($indentStyle)
150+
{
151+
return $this->indentChars[$indentStyle];
152+
}
153+
154+
/**
155+
* Get indent size for pretty printed JSON string.
156+
*
157+
* @return int|null
158+
*/
159+
protected function getPrettyPrintIndentSize()
160+
{
161+
return isset($this->options['indent_size'])
162+
? (int) $this->options['indent_size']
163+
: null;
164+
}
165+
166+
/**
167+
* Determine if indent style is allowed to have various indent size.
168+
*
169+
* @param string $indentStyle
170+
*
171+
* @return bool
172+
*/
173+
protected function hasVariousIndentSize($indentStyle)
174+
{
175+
return in_array($indentStyle, $this->hasVariousIndentSize);
176+
}
177+
178+
/**
179+
* Perform indentation for pretty printed JSON string with a given
180+
* indent char, repeated N times, as determined by indent size.
181+
*
182+
* @param string $jsonString JSON string, which must be indented
183+
* @param string $indentChar Char, used for indent (default is tab)
184+
* @param int $indentSize Number of times to repeat indent char per one indent level
185+
* @param int $defaultSpaces Default number of indent spaces after json_encode()
186+
*
187+
* @return string
188+
*/
189+
protected function peformIndentation($jsonString, $indentChar = "\t", $indentSize = 1, $defaultSpaces = 4)
190+
{
191+
$pattern = '/(^|\G) {'.$defaultSpaces.'}/m';
192+
$replacement = str_repeat($indentChar, $indentSize).'$1';
193+
194+
return preg_replace($pattern, $replacement, $jsonString);
195+
}
196+
}

src/Http/Response/Format/Jsonp.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,18 +58,20 @@ public function getContentType()
5858
}
5959

6060
/**
61-
* Encode the content to its JSON representation.
61+
* Encode the content to its JSONP representation.
6262
*
63-
* @param string $content
63+
* @param mixed $content
6464
*
6565
* @return string
6666
*/
6767
protected function encode($content)
6868
{
69+
$jsonString = parent::encode($content);
70+
6971
if ($this->hasValidCallback()) {
70-
return sprintf('%s(%s);', $this->getCallback(), json_encode($content));
72+
return sprintf('%s(%s);', $this->getCallback(), $jsonString);
7173
}
7274

73-
return parent::encode($content);
75+
return $jsonString;
7476
}
7577
}

0 commit comments

Comments
 (0)