1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237: 238: 239: 240: 241: 242: 243: 244: 245: 246: 247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281: 282: 283: 284: 285: 286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296: 297: 298: 299: 300: 301: 302: 303: 304: 305: 306: 307: 308: 309: 310: 311: 312: 313: 314: 315: 316: 317: 318: 319: 320: 321: 322: 323: 324: 325: 326: 327: 328: 329: 330: 331: 332: 333: 334: 335: 336: 337: 338: 339: 340: 341: 342: 343: 344: 345: 346: 347: 348: 349: 350: 351: 352: 353: 354: 355: 356: 357: 358: 359: 360: 361: 362: 363: 364: 365: 366: 367: 368: 369: 370: 371: 372: 373: 374: 375: 376: 377: 378: 379: 380: 381: 382: 383: 384: 385: 386: 387: 388: 389: 390: 391: 392: 393: 394: 395: 396: 397: 398: 399: 400: 401: 402: 403: 404: 405: 406: 407: 408: 409: 410: 411: 412: 413: 414: 415: 416: 417: 418: 419: 420: 421: 422: 423: 424: 425: 426: 427: 428: 429: 430: 431: 432: 433: 434: 435: 436: 437: 438: 439: 440: 441: 442: 443: 444: 445: 446: 447: 448: 449: 450: 451: 452: 453: 454: 455: 456: 457: 458: 459: 460: 461: 462: 463: 464: 465: 466: 467: 468: 469: 470: 471: 472: 473: 474: 475: 476: 477: 478: 479: 480: 481: 482: 483: 484: 485: 486: 487: 488: 489: 490: 491: 492: 493: 494: 495: 496: 497: 498: 499: 500: 501: 502: 503: 504: 505: 506: 507: 508: 509: 510: 511: 512: 513: 514: 515: 516: 517: 518: 519: 520: 521: 522: 523: 524: 525: 526: 527: 528: 529: 530: 531: 532: 533: 534: 535: 536: 537: 538: 539: 540: 541: 542: 543: 544: 545: 546: 547: 548: 549: 550: 551: 552: 553: 554: 555: 556: 557: 558: 559: 560: 561: 562: 563: 564: 565: 566: 567: 568: 569: 570: 571: 572: 573: 574: 575: 576: 577: 578: 579: 580: 581: 582: 583: 584: 585: 586: 587: 588: 589: 590: 591:
<?php
/*
* This file is part of the Webmozart JSON package.
*
* (c) Bernhard Schussek <bschussek@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Webmozart\Json;
/**
* Encodes data as JSON.
*
* @since 1.0
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class JsonEncoder
{
/**
* Encode a value as JSON array.
*/
const JSON_ARRAY = 1;
/**
* Encode a value as JSON object.
*/
const JSON_OBJECT = 2;
/**
* Encode a value as JSON string.
*/
const JSON_STRING = 3;
/**
* Encode a value as JSON integer or float.
*/
const JSON_NUMBER = 4;
/**
* @var JsonValidator
*/
private $validator;
/**
* @var int
*/
private $arrayEncoding = self::JSON_ARRAY;
/**
* @var int
*/
private $numericEncoding = self::JSON_STRING;
/**
* @var bool
*/
private $gtLtEscaped = false;
/**
* @var bool
*/
private $ampersandEscaped = false;
/**
* @var bool
*/
private $singleQuoteEscaped = false;
/**
* @var bool
*/
private $doubleQuoteEscaped = false;
/**
* @var bool
*/
private $slashEscaped = true;
/**
* @var bool
*/
private $unicodeEscaped = true;
/**
* @var bool
*/
private $prettyPrinting = false;
/**
* @var bool
*/
private $terminatedWithLineFeed = false;
/**
* @var int
*/
private $maxDepth = 512;
/**
* Creates a new encoder.
*/
public function __construct()
{
$this->validator = new JsonValidator();
}
/**
* Encodes data as JSON.
*
* If a schema is passed, the value is validated against that schema before
* encoding. The schema may be passed as file path or as object returned
* from `JsonDecoder::decodeFile($schemaFile)`.
*
* You can adjust the decoding with the various setters in this class.
*
* @param mixed $data The data to encode.
* @param string|object $schema The schema file or object.
*
* @return string The JSON string.
*
* @throws EncodingFailedException If the data could not be encoded.
* @throws ValidationFailedException If the data fails schema validation.
* @throws InvalidSchemaException If the schema is invalid.
*/
public function encode($data, $schema = null)
{
if (null !== $schema) {
$errors = $this->validator->validate($data, $schema);
if (count($errors) > 0) {
throw ValidationFailedException::fromErrors($errors);
}
}
$options = 0;
if (self::JSON_OBJECT === $this->arrayEncoding) {
$options |= JSON_FORCE_OBJECT;
}
if (self::JSON_NUMBER === $this->numericEncoding) {
$options |= JSON_NUMERIC_CHECK;
}
if ($this->gtLtEscaped) {
$options |= JSON_HEX_TAG;
}
if ($this->ampersandEscaped) {
$options |= JSON_HEX_AMP;
}
if ($this->singleQuoteEscaped) {
$options |= JSON_HEX_APOS;
}
if ($this->doubleQuoteEscaped) {
$options |= JSON_HEX_QUOT;
}
if (PHP_VERSION_ID >= 50400) {
if (!$this->slashEscaped) {
$options |= JSON_UNESCAPED_SLASHES;
}
if (!$this->unicodeEscaped) {
$options |= JSON_UNESCAPED_UNICODE;
}
if ($this->prettyPrinting) {
$options |= JSON_PRETTY_PRINT;
}
}
if (PHP_VERSION_ID >= 50500) {
$maxDepth = $this->maxDepth;
// We subtract 1 from the max depth to make JsonDecoder and
// JsonEncoder consistent. json_encode() and json_decode() behave
// differently for their depth values. See the test cases for
// examples.
// HHVM does not have this inconsistency.
if (!defined('HHVM_VERSION')) {
--$maxDepth;
}
$encoded = json_encode($data, $options, $maxDepth);
} else {
$encoded = json_encode($data, $options);
}
if (false === $encoded) {
throw new EncodingFailedException(sprintf(
'The data could not be encoded as JSON: %s',
JsonError::getLastErrorMessage()
), json_last_error());
}
if ($this->terminatedWithLineFeed) {
$encoded .= "\n";
}
return $encoded;
}
/**
* Encodes data into a JSON file.
*
* @param mixed $data The data to encode.
* @param string $file The path where the JSON file will be stored.
* @param string|object $schema The schema file or object.
*
* @throws EncodingFailedException If the data could not be encoded.
* @throws ValidationFailedException If the data fails schema validation.
* @throws InvalidSchemaException If the schema is invalid.
*
* @see encode
*/
public function encodeFile($data, $file, $schema = null)
{
try {
// Right now, it's sufficient to just write the file. In the future,
// this will diff existing files with the given data and only do
// in-place modifications where necessary.
file_put_contents($file, $this->encode($data, $schema));
} catch (EncodingFailedException $e) {
// Add the file name to the exception
throw new EncodingFailedException(sprintf(
'An error happened while encoding %s: %s',
$file,
$e->getMessage()
), $e->getCode(), $e);
} catch (ValidationFailedException $e) {
// Add the file name to the exception
throw new ValidationFailedException(sprintf(
"Validation failed while encoding %s:\n%s",
$file,
$e->getErrorsAsString()
), $e->getErrors(), $e->getCode(), $e);
} catch (InvalidSchemaException $e) {
// Add the file name to the exception
throw new InvalidSchemaException(sprintf(
'An error happened while encoding %s: %s',
$file,
$e->getMessage()
), $e->getCode(), $e);
}
}
/**
* Returns the encoding of non-associative arrays.
*
* @return int One of the constants {@link JSON_OBJECT} and {@link JSON_ARRAY}.
*/
public function getArrayEncoding()
{
return $this->arrayEncoding;
}
/**
* Sets the encoding of non-associative arrays.
*
* By default, non-associative arrays are decoded as JSON arrays.
*
* @param int $encoding One of the constants {@link JSON_OBJECT} and {@link JSON_ARRAY}.
*
* @throws \InvalidArgumentException If the passed encoding is invalid.
*/
public function setArrayEncoding($encoding)
{
if (self::JSON_ARRAY !== $encoding && self::JSON_OBJECT !== $encoding) {
throw new \InvalidArgumentException(sprintf(
'Expected JsonEncoder::JSON_ARRAY or JsonEncoder::JSON_OBJECT. '.
'Got: %s',
$encoding
));
}
$this->arrayEncoding = $encoding;
}
/**
* Returns the encoding of numeric strings.
*
* @return int One of the constants {@link JSON_STRING} and {@link JSON_NUMBER}.
*/
public function getNumericEncoding()
{
return $this->numericEncoding;
}
/**
* Sets the encoding of numeric strings.
*
* By default, non-associative arrays are decoded as JSON strings.
*
* @param int $encoding One of the constants {@link JSON_STRING} and {@link JSON_NUMBER}.
*
* @throws \InvalidArgumentException If the passed encoding is invalid.
*/
public function setNumericEncoding($encoding)
{
if (self::JSON_NUMBER !== $encoding && self::JSON_STRING !== $encoding) {
throw new \InvalidArgumentException(sprintf(
'Expected JsonEncoder::JSON_NUMBER or JsonEncoder::JSON_STRING. '.
'Got: %s',
$encoding
));
}
$this->numericEncoding = $encoding;
}
/**
* Returns whether ampersands (&) are escaped.
*
* If `true`, ampersands will be escaped as "\u0026".
*
* By default, ampersands are not escaped.
*
* @return bool Whether ampersands are escaped.
*/
public function isAmpersandEscaped()
{
return $this->ampersandEscaped;
}
/**
* Sets whether ampersands (&) should be escaped.
*
* If `true`, ampersands will be escaped as "\u0026".
*
* By default, ampersands are not escaped.
*
* @param bool $enabled Whether ampersands should be escaped.
*/
public function setEscapeAmpersand($enabled)
{
$this->ampersandEscaped = $enabled;
}
/**
* Returns whether double quotes (") are escaped.
*
* If `true`, double quotes will be escaped as "\u0022".
*
* By default, double quotes are not escaped.
*
* @return bool Whether double quotes are escaped.
*/
public function isDoubleQuoteEscaped()
{
return $this->doubleQuoteEscaped;
}
/**
* Sets whether double quotes (") should be escaped.
*
* If `true`, double quotes will be escaped as "\u0022".
*
* By default, double quotes are not escaped.
*
* @param bool $enabled Whether double quotes should be escaped.
*/
public function setEscapeDoubleQuote($enabled)
{
$this->doubleQuoteEscaped = $enabled;
}
/**
* Returns whether single quotes (') are escaped.
*
* If `true`, single quotes will be escaped as "\u0027".
*
* By default, single quotes are not escaped.
*
* @return bool Whether single quotes are escaped.
*/
public function isSingleQuoteEscaped()
{
return $this->singleQuoteEscaped;
}
/**
* Sets whether single quotes (") should be escaped.
*
* If `true`, single quotes will be escaped as "\u0027".
*
* By default, single quotes are not escaped.
*
* @param bool $enabled Whether single quotes should be escaped.
*/
public function setEscapeSingleQuote($enabled)
{
$this->singleQuoteEscaped = $enabled;
}
/**
* Returns whether forward slashes (/) are escaped.
*
* If `true`, forward slashes will be escaped as "\/".
*
* By default, forward slashes are not escaped.
*
* @return bool Whether forward slashes are escaped.
*/
public function isSlashEscaped()
{
return $this->slashEscaped;
}
/**
* Sets whether forward slashes (") should be escaped.
*
* If `true`, forward slashes will be escaped as "\/".
*
* By default, forward slashes are not escaped.
*
* @param bool $enabled Whether forward slashes should be escaped.
*/
public function setEscapeSlash($enabled)
{
$this->slashEscaped = $enabled;
}
/**
* Returns whether greater than/less than symbols (>, <) are escaped.
*
* If `true`, greater than will be escaped as "\u003E" and less than as
* "\u003C".
*
* By default, greater than/less than symbols are not escaped.
*
* @return bool Whether greater than/less than symbols are escaped.
*/
public function isGtLtEscaped()
{
return $this->gtLtEscaped;
}
/**
* Sets whether greater than/less than symbols (>, <) should be escaped.
*
* If `true`, greater than will be escaped as "\u003E" and less than as
* "\u003C".
*
* By default, greater than/less than symbols are not escaped.
*
* @param bool $enabled Whether greater than/less than should be escaped.
*/
public function setEscapeGtLt($enabled)
{
$this->gtLtEscaped = $enabled;
}
/**
* Returns whether unicode characters are escaped.
*
* If `true`, unicode characters will be escaped as hexadecimals strings.
* For example, "ü" will be escaped as "\u00fc".
*
* By default, unicode characters are escaped.
*
* @return bool Whether unicode characters are escaped.
*/
public function isUnicodeEscaped()
{
return $this->unicodeEscaped;
}
/**
* Sets whether unicode characters should be escaped.
*
* If `true`, unicode characters will be escaped as hexadecimals strings.
* For example, "ü" will be escaped as "\u00fc".
*
* By default, unicode characters are escaped.
*
* @param bool $enabled Whether unicode characters should be escaped.
*/
public function setEscapeUnicode($enabled)
{
$this->unicodeEscaped = $enabled;
}
/**
* Returns whether JSON strings are formatted for better readability.
*
* If `true`, line breaks will be added after object properties and array
* entries. Each new nesting level will be indented by four spaces.
*
* By default, pretty printing is not enabled.
*
* @return bool Whether JSON strings are formatted.
*/
public function isPrettyPrinting()
{
return $this->prettyPrinting;
}
/**
* Sets whether JSON strings should be formatted for better readability.
*
* If `true`, line breaks will be added after object properties and array
* entries. Each new nesting level will be indented by four spaces.
*
* By default, pretty printing is not enabled.
*
* @param bool $prettyPrinting Whether JSON strings should be formatted.
*/
public function setPrettyPrinting($prettyPrinting)
{
$this->prettyPrinting = $prettyPrinting;
}
/**
* Returns whether JSON strings are terminated with a line feed.
*
* By default, JSON strings are not terminated with a line feed.
*
* @return bool Whether JSON strings are terminated with a line feed.
*/
public function isTerminatedWithLineFeed()
{
return $this->terminatedWithLineFeed;
}
/**
* Sets whether JSON strings should be terminated with a line feed.
*
* By default, JSON strings are not terminated with a line feed.
*
* @param bool $enabled Whether JSON strings should be terminated with a
* line feed.
*/
public function setTerminateWithLineFeed($enabled)
{
$this->terminatedWithLineFeed = $enabled;
}
/**
* Returns the maximum recursion depth.
*
* A depth of zero means that objects are not allowed. A depth of one means
* only one level of objects or arrays is allowed.
*
* @return int The maximum recursion depth.
*/
public function getMaxDepth()
{
return $this->maxDepth;
}
/**
* Sets the maximum recursion depth.
*
* If the depth is exceeded during encoding, an {@link EncodingFailedException}
* will be thrown.
*
* A depth of zero means that objects are not allowed. A depth of one means
* only one level of objects or arrays is allowed.
*
* @param int $maxDepth The maximum recursion depth.
*
* @throws \InvalidArgumentException If the depth is not an integer greater
* than or equal to zero.
*/
public function setMaxDepth($maxDepth)
{
if (!is_int($maxDepth)) {
throw new \InvalidArgumentException(sprintf(
'The maximum depth should be an integer. Got: %s',
is_object($maxDepth) ? get_class($maxDepth) : gettype($maxDepth)
));
}
if ($maxDepth < 1) {
throw new \InvalidArgumentException(sprintf(
'The maximum depth should 1 or greater. Got: %s',
$maxDepth
));
}
$this->maxDepth = $maxDepth;
}
}