1
|
|
|
<?php |
2
|
|
|
namespace PSB\Core\Serialization\Json; |
3
|
|
|
|
4
|
|
|
|
5
|
|
|
use PSB\Core\Exception\JsonSerializerException; |
6
|
|
|
use PSB\Core\Util\Guard; |
7
|
|
|
|
8
|
|
|
class JsonEncoder |
9
|
|
|
{ |
10
|
|
|
const FLOAT_CASTER = 'PSBFloat'; |
11
|
|
|
|
12
|
|
|
const KEY_UTF8ENCODED = 1; |
13
|
|
|
const VALUE_UTF8ENCODED = 2; |
14
|
|
|
|
15
|
|
|
/** |
16
|
|
|
* @var int |
17
|
|
|
*/ |
18
|
|
|
private $jsonEncodeOptions; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* @var string |
22
|
|
|
*/ |
23
|
|
|
private $encodingAnnotation; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* @param int $jsonEncodeOptions |
27
|
|
|
* @param string $encodingAnnotation |
28
|
|
|
*/ |
29
|
8 |
|
public function __construct($jsonEncodeOptions = JSON_UNESCAPED_UNICODE, $encodingAnnotation = '@utf8encoded') |
30
|
|
|
{ |
31
|
8 |
|
Guard::againstNullAndEmpty('encodingAnnotation', $encodingAnnotation); |
32
|
|
|
|
33
|
8 |
|
if (PHP_VERSION_ID >= 50606) { |
34
|
|
|
$jsonEncodeOptions |= JSON_PRESERVE_ZERO_FRACTION; |
35
|
|
|
} |
36
|
|
|
|
37
|
8 |
|
$this->jsonEncodeOptions = $jsonEncodeOptions; |
38
|
8 |
|
$this->encodingAnnotation = $encodingAnnotation; |
39
|
8 |
|
} |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* @param array $data |
43
|
|
|
* |
44
|
|
|
* @return string |
45
|
|
|
*/ |
46
|
5 |
|
public function encode(array $data) |
47
|
|
|
{ |
48
|
5 |
|
$data = $this->escapeFloatsIfNeeded($data); |
49
|
|
|
|
50
|
5 |
|
$json = json_encode($data, $this->jsonEncodeOptions); |
51
|
5 |
|
if ($json === false || json_last_error() != JSON_ERROR_NONE) { |
52
|
3 |
|
if (json_last_error() != JSON_ERROR_UTF8) { |
53
|
1 |
|
throw new JsonSerializerException('Invalid data to encode to JSON. Error: ' . json_last_error_msg()); |
54
|
|
|
} |
55
|
|
|
|
56
|
2 |
|
$data = $this->encodeNonUtf8ToUtf8($data); |
57
|
2 |
|
$json = json_encode($data, $this->jsonEncodeOptions); |
58
|
|
|
|
59
|
2 |
|
if ($json === false || json_last_error() != JSON_ERROR_NONE) { |
60
|
|
|
throw new JsonSerializerException('Invalid data to encode to JSON. Error: ' . json_last_error_msg()); |
61
|
|
|
} |
62
|
2 |
|
} |
63
|
|
|
|
64
|
4 |
|
return $this->unescapeFloatsIfNeeded($json); |
65
|
|
|
} |
66
|
|
|
|
67
|
|
|
/** |
68
|
|
|
* @param string $json |
69
|
|
|
* |
70
|
|
|
* @return array |
71
|
|
|
*/ |
72
|
4 |
|
public function decode($json) |
73
|
|
|
{ |
74
|
4 |
|
$data = json_decode($json, true); |
75
|
4 |
|
if ($data === null && json_last_error() != JSON_ERROR_NONE) { |
76
|
1 |
|
throw new JsonSerializerException('Invalid JSON to unserialize.'); |
77
|
|
|
} |
78
|
|
|
|
79
|
3 |
|
if (!is_array($data)) { |
80
|
|
|
throw new JsonSerializerException('Given JSON cannot represent an object.'); |
81
|
|
|
} |
82
|
|
|
|
83
|
3 |
|
if (mb_strpos($json, $this->encodingAnnotation) !== false) { |
84
|
2 |
|
$data = $this->decodeNonUtf8FromUtf8($data); |
85
|
2 |
|
} |
86
|
|
|
|
87
|
3 |
|
return $data; |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* @param array $data |
92
|
|
|
* |
93
|
|
|
* @return array |
94
|
|
|
*/ |
95
|
5 |
|
private function escapeFloatsIfNeeded(array $data) |
96
|
|
|
{ |
97
|
5 |
|
if (PHP_VERSION_ID >= 50606) { |
98
|
|
|
return $data; |
99
|
|
|
} |
100
|
|
|
|
101
|
5 |
|
array_walk_recursive( |
102
|
5 |
|
$data, |
103
|
5 |
|
function (&$value) { |
104
|
5 |
|
if (is_float($value) && ctype_digit((string)$value)) { |
105
|
|
|
// Due to PHP bug #50224, floats with no decimals are converted to integers when encoded |
106
|
1 |
|
$value = '(' . self::FLOAT_CASTER . ')' . $value . '.0'; |
107
|
1 |
|
} |
108
|
5 |
|
} |
109
|
5 |
|
); |
110
|
|
|
|
111
|
5 |
|
return $data; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* @param string $json |
116
|
|
|
* |
117
|
|
|
* @return string |
118
|
|
|
*/ |
119
|
4 |
|
private function unescapeFloatsIfNeeded($json) |
120
|
|
|
{ |
121
|
4 |
|
if (PHP_VERSION_ID >= 50606) { |
122
|
|
|
return $json; |
123
|
|
|
} |
124
|
|
|
|
125
|
4 |
|
$prevEncoding = mb_regex_encoding(); |
126
|
4 |
|
mb_regex_encoding('UTF-8'); |
127
|
4 |
|
$json = mb_ereg_replace('"\(' . self::FLOAT_CASTER . '\)([^"]+)"', '\1', $json); |
128
|
4 |
|
mb_regex_encoding($prevEncoding); |
129
|
|
|
|
130
|
4 |
|
return $json; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* @param array $serializedData |
135
|
|
|
* |
136
|
|
|
* @return array |
137
|
|
|
*/ |
138
|
2 |
|
private function encodeNonUtf8ToUtf8(array $serializedData) |
139
|
|
|
{ |
140
|
2 |
|
$encodedKeys = []; |
141
|
2 |
|
$encodedData = []; |
142
|
2 |
|
foreach ($serializedData as $key => $value) { |
143
|
2 |
|
if (is_array($value)) { |
144
|
|
|
$value = $this->encodeNonUtf8ToUtf8($value); |
145
|
|
|
} |
146
|
|
|
|
147
|
2 |
View Code Duplication |
if (!mb_check_encoding($key, 'UTF-8')) { |
148
|
1 |
|
$key = mb_convert_encoding($key, 'UTF-8', '8bit'); |
149
|
1 |
|
$encodedKeys[$key] = (isset($encodedKeys[$key]) ? $encodedKeys[$key] : 0) | static::KEY_UTF8ENCODED; |
150
|
1 |
|
} |
151
|
|
|
|
152
|
2 |
View Code Duplication |
if (is_string($value)) { |
|
|
|
|
153
|
2 |
|
if (!mb_check_encoding($value, 'UTF-8')) { |
154
|
2 |
|
$value = mb_convert_encoding($value, 'UTF-8', '8bit'); |
155
|
2 |
|
$encodedKeys[$key] = (isset($encodedKeys[$key]) ? $encodedKeys[$key] : 0) | static::VALUE_UTF8ENCODED; |
156
|
2 |
|
} |
157
|
2 |
|
} |
158
|
|
|
|
159
|
2 |
|
$encodedData[$key] = $value; |
160
|
2 |
|
} |
161
|
|
|
|
162
|
2 |
|
if (!empty($encodedKeys)) { |
163
|
2 |
|
$encodedData[$this->encodingAnnotation] = $encodedKeys; |
164
|
2 |
|
} |
165
|
|
|
|
166
|
2 |
|
return $encodedData; |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* @param array $data |
171
|
|
|
* |
172
|
|
|
* @return array |
173
|
|
|
*/ |
174
|
2 |
|
private function decodeNonUtf8FromUtf8(array $data) |
175
|
|
|
{ |
176
|
2 |
|
$encodedKeys = []; |
177
|
2 |
|
if (isset($data[$this->encodingAnnotation])) { |
178
|
2 |
|
$encodedKeys = $data[$this->encodingAnnotation]; |
179
|
2 |
|
unset($data[$this->encodingAnnotation]); |
180
|
2 |
|
} |
181
|
|
|
|
182
|
2 |
|
$decodedData = []; |
183
|
2 |
|
foreach ($data as $key => $value) { |
184
|
2 |
|
if (is_array($value)) { |
185
|
|
|
$value = $this->decodeNonUtf8FromUtf8($value); |
186
|
|
|
} |
187
|
|
|
|
188
|
2 |
|
if (isset($encodedKeys[$key])) { |
189
|
2 |
|
$originalKey = $key; |
190
|
2 |
|
if ($encodedKeys[$key] & static::KEY_UTF8ENCODED) { |
191
|
1 |
|
$key = mb_convert_encoding($key, '8bit', 'UTF-8'); |
192
|
1 |
|
} |
193
|
2 |
|
if ($encodedKeys[$originalKey] & static::VALUE_UTF8ENCODED) { |
194
|
2 |
|
$value = mb_convert_encoding($value, '8bit', 'UTF-8'); |
195
|
2 |
|
} |
196
|
2 |
|
} |
197
|
|
|
|
198
|
2 |
|
$decodedData[$key] = $value; |
199
|
2 |
|
} |
200
|
|
|
|
201
|
2 |
|
return $decodedData; |
202
|
|
|
} |
203
|
|
|
} |
204
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.