Completed
Push — devel ( a59919...df32c1 )
by Alex
8s
created

JsonEncoder::decode()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5.025

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 17
ccs 9
cts 10
cp 0.9
rs 8.8571
cc 5
eloc 9
nc 4
nop 1
crap 5.025
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)) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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