BaseJson.php 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\helpers;
  8. use yii\base\Arrayable;
  9. use yii\base\InvalidArgumentException;
  10. use yii\web\JsExpression;
  11. use yii\base\Model;
  12. /**
  13. * BaseJson provides concrete implementation for [[Json]].
  14. *
  15. * Do not use BaseJson. Use [[Json]] instead.
  16. *
  17. * @author Qiang Xue <qiang.xue@gmail.com>
  18. * @since 2.0
  19. */
  20. class BaseJson
  21. {
  22. /**
  23. * @var bool|null Enables human readable output a.k.a. Pretty Print.
  24. * This can useful for debugging during development but is not recommended in a production environment!
  25. * In case `prettyPrint` is `null` (default) the `options` passed to `encode` functions will not be changed.
  26. * @since 2.0.43
  27. */
  28. public static $prettyPrint = null;
  29. /**
  30. * List of JSON Error messages assigned to constant names for better handling of version differences.
  31. * @var array
  32. * @since 2.0.7
  33. */
  34. public static $jsonErrorMessages = [
  35. 'JSON_ERROR_DEPTH' => 'The maximum stack depth has been exceeded.',
  36. 'JSON_ERROR_STATE_MISMATCH' => 'Invalid or malformed JSON.',
  37. 'JSON_ERROR_CTRL_CHAR' => 'Control character error, possibly incorrectly encoded.',
  38. 'JSON_ERROR_SYNTAX' => 'Syntax error.',
  39. 'JSON_ERROR_UTF8' => 'Malformed UTF-8 characters, possibly incorrectly encoded.', // PHP 5.3.3
  40. 'JSON_ERROR_RECURSION' => 'One or more recursive references in the value to be encoded.', // PHP 5.5.0
  41. 'JSON_ERROR_INF_OR_NAN' => 'One or more NAN or INF values in the value to be encoded', // PHP 5.5.0
  42. 'JSON_ERROR_UNSUPPORTED_TYPE' => 'A value of a type that cannot be encoded was given', // PHP 5.5.0
  43. ];
  44. /**
  45. * Encodes the given value into a JSON string.
  46. *
  47. * The method enhances `json_encode()` by supporting JavaScript expressions.
  48. * In particular, the method will not encode a JavaScript expression that is
  49. * represented in terms of a [[JsExpression]] object.
  50. *
  51. * Note that data encoded as JSON must be UTF-8 encoded according to the JSON specification.
  52. * You must ensure strings passed to this method have proper encoding before passing them.
  53. *
  54. * @param mixed $value the data to be encoded.
  55. * @param int $options the encoding options. For more details please refer to
  56. * <https://secure.php.net/manual/en/function.json-encode.php>. Default is `JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE`.
  57. * @return string the encoding result.
  58. * @throws InvalidArgumentException if there is any encoding error.
  59. */
  60. public static function encode($value, $options = 320)
  61. {
  62. $expressions = [];
  63. $value = static::processData($value, $expressions, uniqid('', true));
  64. set_error_handler(function () {
  65. static::handleJsonError(JSON_ERROR_SYNTAX);
  66. }, E_WARNING);
  67. if (static::$prettyPrint === true) {
  68. $options |= JSON_PRETTY_PRINT;
  69. } elseif (static::$prettyPrint === false) {
  70. $options &= ~JSON_PRETTY_PRINT;
  71. }
  72. $json = json_encode($value, $options);
  73. restore_error_handler();
  74. static::handleJsonError(json_last_error());
  75. return $expressions === [] ? $json : strtr($json, $expressions);
  76. }
  77. /**
  78. * Encodes the given value into a JSON string HTML-escaping entities so it is safe to be embedded in HTML code.
  79. *
  80. * The method enhances `json_encode()` by supporting JavaScript expressions.
  81. * In particular, the method will not encode a JavaScript expression that is
  82. * represented in terms of a [[JsExpression]] object.
  83. *
  84. * Note that data encoded as JSON must be UTF-8 encoded according to the JSON specification.
  85. * You must ensure strings passed to this method have proper encoding before passing them.
  86. *
  87. * @param mixed $value the data to be encoded
  88. * @return string the encoding result
  89. * @since 2.0.4
  90. * @throws InvalidArgumentException if there is any encoding error
  91. */
  92. public static function htmlEncode($value)
  93. {
  94. return static::encode($value, JSON_UNESCAPED_UNICODE | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS);
  95. }
  96. /**
  97. * Decodes the given JSON string into a PHP data structure.
  98. * @param string $json the JSON string to be decoded
  99. * @param bool $asArray whether to return objects in terms of associative arrays.
  100. * @return mixed the PHP data
  101. * @throws InvalidArgumentException if there is any decoding error
  102. */
  103. public static function decode($json, $asArray = true)
  104. {
  105. if (is_array($json)) {
  106. throw new InvalidArgumentException('Invalid JSON data.');
  107. } elseif ($json === null || $json === '') {
  108. return null;
  109. }
  110. $decode = json_decode((string) $json, $asArray);
  111. static::handleJsonError(json_last_error());
  112. return $decode;
  113. }
  114. /**
  115. * Handles [[encode()]] and [[decode()]] errors by throwing exceptions with the respective error message.
  116. *
  117. * @param int $lastError error code from [json_last_error()](https://secure.php.net/manual/en/function.json-last-error.php).
  118. * @throws InvalidArgumentException if there is any encoding/decoding error.
  119. * @since 2.0.6
  120. */
  121. protected static function handleJsonError($lastError)
  122. {
  123. if ($lastError === JSON_ERROR_NONE) {
  124. return;
  125. }
  126. $availableErrors = [];
  127. foreach (static::$jsonErrorMessages as $const => $message) {
  128. if (defined($const)) {
  129. $availableErrors[constant($const)] = $message;
  130. }
  131. }
  132. if (isset($availableErrors[$lastError])) {
  133. throw new InvalidArgumentException($availableErrors[$lastError], $lastError);
  134. }
  135. throw new InvalidArgumentException('Unknown JSON encoding/decoding error.');
  136. }
  137. /**
  138. * Pre-processes the data before sending it to `json_encode()`.
  139. * @param mixed $data the data to be processed
  140. * @param array $expressions collection of JavaScript expressions
  141. * @param string $expPrefix a prefix internally used to handle JS expressions
  142. * @return mixed the processed data
  143. */
  144. protected static function processData($data, &$expressions, $expPrefix)
  145. {
  146. if (is_object($data)) {
  147. if ($data instanceof JsExpression) {
  148. $token = "!{[$expPrefix=" . count($expressions) . ']}!';
  149. $expressions['"' . $token . '"'] = $data->expression;
  150. return $token;
  151. }
  152. if ($data instanceof \JsonSerializable) {
  153. return static::processData($data->jsonSerialize(), $expressions, $expPrefix);
  154. }
  155. if ($data instanceof \DateTimeInterface) {
  156. return static::processData((array)$data, $expressions, $expPrefix);
  157. }
  158. if ($data instanceof Arrayable) {
  159. $data = $data->toArray();
  160. } elseif ($data instanceof \SimpleXMLElement) {
  161. $data = (array) $data;
  162. } else {
  163. $result = [];
  164. foreach ($data as $name => $value) {
  165. $result[$name] = $value;
  166. }
  167. $data = $result;
  168. }
  169. if ($data === []) {
  170. return new \stdClass();
  171. }
  172. }
  173. if (is_array($data)) {
  174. foreach ($data as $key => $value) {
  175. if (is_array($value) || is_object($value)) {
  176. $data[$key] = static::processData($value, $expressions, $expPrefix);
  177. }
  178. }
  179. }
  180. return $data;
  181. }
  182. /**
  183. * Generates a summary of the validation errors.
  184. * @param Model|Model[] $models the model(s) whose validation errors are to be displayed.
  185. * @param array $options the tag options in terms of name-value pairs. The following options are specially handled:
  186. *
  187. * - showAllErrors: boolean, if set to true every error message for each attribute will be shown otherwise
  188. * only the first error message for each attribute will be shown. Defaults to `false`.
  189. *
  190. * @return string the generated error summary
  191. * @since 2.0.14
  192. */
  193. public static function errorSummary($models, $options = [])
  194. {
  195. $showAllErrors = ArrayHelper::remove($options, 'showAllErrors', false);
  196. $lines = self::collectErrors($models, $showAllErrors);
  197. return json_encode($lines);
  198. }
  199. /**
  200. * Return array of the validation errors
  201. * @param Model|Model[] $models the model(s) whose validation errors are to be displayed.
  202. * @param $showAllErrors boolean, if set to true every error message for each attribute will be shown otherwise
  203. * only the first error message for each attribute will be shown.
  204. * @return array of the validation errors
  205. * @since 2.0.14
  206. */
  207. private static function collectErrors($models, $showAllErrors)
  208. {
  209. $lines = [];
  210. if (!is_array($models)) {
  211. $models = [$models];
  212. }
  213. foreach ($models as $model) {
  214. $lines = array_unique(array_merge($lines, $model->getErrorSummary($showAllErrors)));
  215. }
  216. return $lines;
  217. }
  218. }