Passed
Push — master ( 52679f...3ad837 )
by Alexander
19:59 queued 10:36
created

Json::encode()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 26
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3.8846

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 3
eloc 12
c 2
b 0
f 0
nc 4
nop 3
dl 0
loc 26
ccs 7
cts 13
cp 0.5385
crap 3.8846
rs 9.8666
1
<?php
2
3
namespace Yiisoft\Json;
4
5
/**
6
 * Json is a helper class providing JSON data encoding and decoding.
7
 * It enhances the PHP built-in functions `json_encode()` and `json_decode()`
8
 * by throwing exceptions when decoding fails.
9
 */
10
final class Json
11
{
12
    private const ERRORS = [
13
        'JSON_ERROR_DEPTH' => 'Maximum stack depth exceeded',
14
        'JSON_ERROR_STATE_MISMATCH' => 'State mismatch (invalid or malformed JSON)',
15
        'JSON_ERROR_CTRL_CHAR' => 'Control character error, possibly incorrectly encoded',
16
        'JSON_ERROR_SYNTAX' => 'Syntax error',
17
        'JSON_ERROR_UTF8' => 'Malformed UTF-8 characters, possibly incorrectly encoded',
18
        'JSON_ERROR_RECURSION' => 'Recursion detected',
19
        'JSON_ERROR_INF_OR_NAN' => 'Inf and NaN cannot be JSON encoded',
20
        'JSON_ERROR_UNSUPPORTED_TYPE' => 'Type is not supported',
21
        'JSON_ERROR_INVALID_PROPERTY_NAME' => 'The decoded property name is invalid',
22
        'JSON_ERROR_UTF16' => 'Single unpaired UTF-16 surrogate in unicode escape',
23
    ];
24
25
    /**
26
     * Encodes the given value into a JSON string.
27
     *
28
     * Note that data encoded as JSON must be UTF-8 encoded according to the JSON specification.
29
     * You must ensure strings passed to this method have proper encoding before passing them.
30
     *
31
     * @param mixed $value the data to be encoded.
32
     * @param int $options the encoding options. For more details please refer to
33
     * <http://www.php.net/manual/en/function.json-encode.php>. Default is `JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR`.
34
     * @param int $depth the maximum depth.
35
     * @return string the encoding result.
36
     * @throws \JsonException if there is any encoding error.
37
     */
38 17
    public static function encode(
39
        $value,
40
        int $options = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR,
41
        int $depth = 512
42
    ): string {
43 17
        $shouldRethrowErrors = self::shouldRethrowErrors($options);
44
45 17
        $value = self::processData($value);
46
47 17
        if ($shouldRethrowErrors) {
48
            set_error_handler(
49
                static function () {
50
                    self::rethrowJsonError(JSON_ERROR_SYNTAX);
51
                },
52
                E_WARNING
53
            );
54
        }
55
56 17
        $json = json_encode($value, $options, $depth);
57
58 16
        if ($shouldRethrowErrors) {
59
            restore_error_handler();
60
            self::rethrowJsonError(json_last_error());
61
        }
62
63 16
        return $json;
64
    }
65
66 19
    private static function shouldRethrowErrors(int $options): bool
67
    {
68 19
        if (!self::hasFlag($options, JSON_THROW_ON_ERROR)) {
69
            return false;
70
        }
71
72 19
        return PHP_VERSION_ID < 70300;
73
    }
74
75 19
    private static function hasFlag(int $flags, int $flag): bool
76
    {
77 19
        return ($flags & $flag) === $flag;
78
    }
79
80
    /**
81
     * Encodes the given value into a JSON string HTML-escaping entities so it is safe to be embedded in HTML code.
82
     *
83
     * Note that data encoded as JSON must be UTF-8 encoded according to the JSON specification.
84
     * You must ensure strings passed to this method have proper encoding before passing them.
85
     *
86
     * @param mixed $value the data to be encoded
87
     * @return string the encoding result
88
     * @throws \JsonException if there is any encoding error
89
     */
90 6
    public static function htmlEncode($value): string
91
    {
92 6
        return self::encode(
93 6
            $value,
94 6
            JSON_UNESCAPED_UNICODE | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_THROW_ON_ERROR
95
        );
96
    }
97
98
    /**
99
     * Decodes the given JSON string into a PHP data structure.
100
     * @param string $json the JSON string to be decoded
101
     * @param bool $asArray whether to return objects in terms of associative arrays.
102
     * @param int $depth the recursion depth.
103
     * @param int $options the decode options.
104
     * @return mixed the PHP data
105
     * @throws \JsonException if there is any decoding error
106
     */
107 5
    public static function decode(
108
        string $json,
109
        bool $asArray = true,
110
        int $depth = 512,
111
        int $options = JSON_THROW_ON_ERROR
112
    ) {
113 5
        if ($json === '') {
114 1
            return null;
115
        }
116 4
        $decode = json_decode($json, $asArray, $depth, $options);
117
118 2
        if (self::shouldRethrowErrors($options)) {
119
            self::rethrowJsonError(json_last_error());
120
        }
121 2
        return $decode;
122
    }
123
124
    /**
125
     * Handles [[encode()]] and [[decode()]] errors by throwing exceptions with the respective error message.
126
     *
127
     * @param int $lastError error code from [json_last_error()](http://php.net/manual/en/function.json-last-error.php).
128
     * @throws \JsonException if there is any encoding/decoding error.
129
     */
130
    private static function rethrowJsonError(int $lastError): void
131
    {
132
        if ($lastError === JSON_ERROR_NONE) {
133
            return;
134
        }
135
        $availableErrors = [];
136
        foreach (self::ERRORS as $constant => $message) {
137
            if (defined($constant)) {
138
                $availableErrors[constant($constant)] = $message;
139
            }
140
        }
141
        if (isset($availableErrors[$lastError])) {
142
            throw new \JsonException($availableErrors[$lastError], $lastError);
143
        }
144
        throw new \JsonException('Unknown JSON encoding/decoding error.');
145
    }
146
147
    /**
148
     * Pre-processes the data before sending it to `json_encode()`.
149
     * @param mixed $data the data to be processed
150
     * @return mixed the processed data
151
     */
152 17
    private static function processData($data)
153
    {
154 17
        if (is_object($data)) {
155 11
            if ($data instanceof \JsonSerializable) {
156 5
                return self::processData($data->jsonSerialize());
157
            }
158
159 8
            if ($data instanceof \SimpleXMLElement) {
160 1
                $data = (array)$data;
161
            } else {
162 7
                $result = [];
163 7
                foreach ($data as $name => $value) {
164 3
                    $result[$name] = $value;
165
                }
166 7
                $data = $result;
167
            }
168 8
            if ($data === []) {
169 4
                return new \stdClass();
170
            }
171
        }
172 14
        if (is_array($data)) {
173 11
            foreach ($data as $key => $value) {
174 9
                if (is_array($value) || is_object($value)) {
175 2
                    $data[$key] = self::processData($value);
176
                }
177
            }
178
        }
179 14
        return $data;
180
    }
181
}
182