JsonEncoder::decodeNonUtf8FromUtf8()   B
last analyzed

Complexity

Conditions 7
Paths 22

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 7.0099

Importance

Changes 0
Metric Value
dl 0
loc 29
ccs 16
cts 17
cp 0.9412
rs 8.5226
c 0
b 0
f 0
cc 7
nc 22
nop 1
crap 7.0099
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 8
            $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
        }
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
        }
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 5
            return $data;
99
        }
100
101
        array_walk_recursive(
102
            $data,
103
            function (&$value) {
104
                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
                    $value = '(' . self::FLOAT_CASTER . ')' . $value . '.0';
107
                }
108
            }
109
        );
110
111
        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 4
            return $json;
123
        }
124
125
        $prevEncoding = mb_regex_encoding();
126
        mb_regex_encoding('UTF-8');
127
        $json = mb_ereg_replace('"\(' . self::FLOAT_CASTER . '\)([^"]+)"', '\1', $json);
128
        mb_regex_encoding($prevEncoding);
129
130
        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
            }
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
                }
157
            }
158
159 2
            $encodedData[$key] = $value;
160
        }
161
162 2
        if (!empty($encodedKeys)) {
163 2
            $encodedData[$this->encodingAnnotation] = $encodedKeys;
164
        }
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
        }
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
                }
193 2
                if ($encodedKeys[$originalKey] & static::VALUE_UTF8ENCODED) {
194 2
                    $value = mb_convert_encoding($value, '8bit', 'UTF-8');
195
                }
196
            }
197
198 2
            $decodedData[$key] = $value;
199
        }
200
201 2
        return $decodedData;
202
    }
203
}
204