Encoder.php 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. <?php
  2. namespace Clue\React\NDJson;
  3. use Evenement\EventEmitter;
  4. use React\Stream\WritableStreamInterface;
  5. /**
  6. * The Encoder / Serializer can be used to write any value, encode it as a JSON text and forward it to an output stream
  7. */
  8. class Encoder extends EventEmitter implements WritableStreamInterface
  9. {
  10. private $output;
  11. private $options;
  12. private $depth;
  13. private $closed = false;
  14. /**
  15. * @param WritableStreamInterface $output
  16. * @param int $options
  17. * @param int $depth (requires PHP 5.5+)
  18. * @throws \InvalidArgumentException
  19. * @throws \BadMethodCallException
  20. */
  21. public function __construct(WritableStreamInterface $output, $options = 0, $depth = 512)
  22. {
  23. // @codeCoverageIgnoreStart
  24. if (\defined('JSON_PRETTY_PRINT') && $options & \JSON_PRETTY_PRINT) {
  25. throw new \InvalidArgumentException('Pretty printing not available for NDJSON');
  26. }
  27. if ($depth !== 512 && \PHP_VERSION < 5.5) {
  28. throw new \BadMethodCallException('Depth parameter is only supported on PHP 5.5+');
  29. }
  30. if (\defined('JSON_THROW_ON_ERROR')) {
  31. $options = $options & ~\JSON_THROW_ON_ERROR;
  32. }
  33. // @codeCoverageIgnoreEnd
  34. $this->output = $output;
  35. if (!$output->isWritable()) {
  36. $this->close();
  37. return;
  38. }
  39. $this->options = $options;
  40. $this->depth = $depth;
  41. $this->output->on('drain', array($this, 'handleDrain'));
  42. $this->output->on('error', array($this, 'handleError'));
  43. $this->output->on('close', array($this, 'close'));
  44. }
  45. public function write($data)
  46. {
  47. if ($this->closed) {
  48. return false;
  49. }
  50. // we have to handle PHP warnings for legacy PHP < 5.5
  51. // certain values (such as INF etc.) emit a warning, but still encode successfully
  52. // @codeCoverageIgnoreStart
  53. if (\PHP_VERSION_ID < 50500) {
  54. $errstr = null;
  55. \set_error_handler(function ($_, $error) use (&$errstr) {
  56. $errstr = $error;
  57. });
  58. // encode data with options given in ctor (depth not supported)
  59. $data = \json_encode($data, $this->options);
  60. // always check error code and match missing error messages
  61. \restore_error_handler();
  62. $errno = \json_last_error();
  63. if (\defined('JSON_ERROR_UTF8') && $errno === \JSON_ERROR_UTF8) {
  64. // const JSON_ERROR_UTF8 added in PHP 5.3.3, but no error message assigned in legacy PHP < 5.5
  65. // this overrides PHP 5.3.14 only: https://3v4l.org/IGP8Z#v5314
  66. $errstr = 'Malformed UTF-8 characters, possibly incorrectly encoded';
  67. } elseif ($errno !== \JSON_ERROR_NONE && $errstr === null) {
  68. // error number present, but no error message applicable
  69. $errstr = 'Unknown error';
  70. }
  71. // abort stream if encoding fails
  72. if ($errno !== \JSON_ERROR_NONE || $errstr !== null) {
  73. $this->handleError(new \RuntimeException('Unable to encode JSON: ' . $errstr, $errno));
  74. return false;
  75. }
  76. } else {
  77. // encode data with options given in ctor
  78. $data = \json_encode($data, $this->options, $this->depth);
  79. // abort stream if encoding fails
  80. if ($data === false && \json_last_error() !== \JSON_ERROR_NONE) {
  81. $this->handleError(new \RuntimeException('Unable to encode JSON: ' . \json_last_error_msg(), \json_last_error()));
  82. return false;
  83. }
  84. }
  85. // @codeCoverageIgnoreEnd
  86. return $this->output->write($data . "\n");
  87. }
  88. public function end($data = null)
  89. {
  90. if ($data !== null) {
  91. $this->write($data);
  92. }
  93. $this->output->end();
  94. }
  95. public function isWritable()
  96. {
  97. return !$this->closed;
  98. }
  99. public function close()
  100. {
  101. if ($this->closed) {
  102. return;
  103. }
  104. $this->closed = true;
  105. $this->output->close();
  106. $this->emit('close');
  107. $this->removeAllListeners();
  108. }
  109. /** @internal */
  110. public function handleDrain()
  111. {
  112. $this->emit('drain');
  113. }
  114. /** @internal */
  115. public function handleError(\Exception $error)
  116. {
  117. $this->emit('error', array($error));
  118. $this->close();
  119. }
  120. }