diff --git a/library/Icinga/Data/StreamInterface.php b/library/Icinga/Data/StreamInterface.php new file mode 100644 index 000000000..98beeafaa --- /dev/null +++ b/library/Icinga/Data/StreamInterface.php @@ -0,0 +1,182 @@ +render()->fpassthru(); + $this->render()->rewind(); + $this->renderBuffer->fpassthru(); } public function __toString() { - return (string) $this->render(); + try { + return (string) $this->render(); + } catch (Exception $e) { + return (string) $e; + } } /** @@ -51,10 +57,10 @@ class Csv $first = true; foreach ($this->query as $row) { if ($first) { - $this->renderBuffer->append($this->renderRow(array_keys((array) $row))); + $this->renderBuffer->write($this->renderRow(array_keys((array) $row))); $first = false; } - $this->renderBuffer->append($this->renderRow(array_values((array) $row))); + $this->renderBuffer->write($this->renderRow(array_values((array) $row))); } } diff --git a/library/Icinga/File/Json.php b/library/Icinga/File/Json.php index 16a84b60b..aa7325c93 100644 --- a/library/Icinga/File/Json.php +++ b/library/Icinga/File/Json.php @@ -3,6 +3,7 @@ namespace Icinga\File; +use Exception; use Icinga\Exception\IcingaException; use Icinga\Util\Buffer; use stdClass; @@ -55,7 +56,8 @@ class Json */ public function dump() { - $this->render()->fpassthru(); + $this->render()->rewind(); + $this->renderBuffer->fpassthru(); } /** @@ -65,7 +67,11 @@ class Json */ public function __toString() { - return (string) $this->render(); + try { + return (string) $this->render(); + } catch (Exception $e) { + return (string) $e; + } } /** @@ -77,19 +83,19 @@ class Json { if ($this->renderBuffer === null) { $this->renderBuffer = new Buffer(); - $this->renderBuffer->append('['); + $this->renderBuffer->write('['); $first = true; foreach ($this->query as $row) { if ($first) { $first = false; } else { - $this->renderBuffer->append(','); + $this->renderBuffer->write(','); } - $this->renderBuffer->append($this->renderRow($row)); + $this->renderBuffer->write($this->renderRow($row)); } - $this->renderBuffer->append(']'); + $this->renderBuffer->write(']'); } return $this->renderBuffer; diff --git a/library/Icinga/Util/Buffer.php b/library/Icinga/Util/Buffer.php index 9395d2b46..78939b905 100644 --- a/library/Icinga/Util/Buffer.php +++ b/library/Icinga/Util/Buffer.php @@ -3,174 +3,16 @@ namespace Icinga\Util; -use Icinga\Exception\IcingaException; - /** * Stores data in memory or a temporary file not to get out of memory */ -class Buffer +class Buffer extends StreamWrapper { - /** - * The actual buffer - * - * @var resource - */ - protected $handle; - - /** - * The amount of bytes currently stored in the buffer - * - * @var int - */ - protected $size = 0; - - /** - * Whether the current position of {@link handle} is the end of file - * - * @var bool - */ - protected $atEOF = true; - /** * Buffer constructor */ public function __construct() { - $this->handle = fopen('php://temp', 'w+b'); - } - - /** - * Append the given data to the buffer - * - * @param string $data - */ - public function append($data) - { - $strlen = strlen($data); - if ($strlen) { - $this->seekToEnd(); - $this->fwrite($this->handle, $data); - $this->size += $strlen; - } - } - - /** - * Get size - * - * @return int - */ - public function getSize() - { - return $this->size; - } - - /** - * Get the data stored in the buffer - * - * @return string - * - * @throws IcingaException In case of an error - */ - public function __toString() - { - $result = ''; - - if ($this->size) { - $this->seekToBegin(); - for (;;) { - $buf = fread($this->handle, $this->size); - if ($buf === '') { - break; - } - $result .= $buf; - } - - if (strlen($result) !== $this->size) { - throw new IcingaException('Couldn\'t read all data from the buffer'); - } - } - - return $result; - } - - /** - * Pass the data stored in the buffer to the user agent (as with {@link fpassthru()}) - */ - public function fpassthru() - { - if ($this->size) { - $this->seekToBegin(); - // fpassthru() returns the amount of bytes passed so we could perform - // the same error check as in __toString(), but throwing exceptions here - // makes no sense as we're already writing the response body - fpassthru($this->handle); - } - } - - /** - * fseek({@link handle}) to the begin of the buffer if not already done - */ - protected function seekToBegin() - { - if ($this->atEOF) { - if ($this->size) { - $this->fseek($this->handle, 0); - } - $this->atEOF = false; - } - } - - /** - * fseek({@link handle}) to the end of the buffer if not already done - */ - protected function seekToEnd() - { - if (! $this->atEOF) { - if ($this->size) { - $this->fseek($this->handle, 0, SEEK_END); - } - $this->atEOF = true; - } - } - - /** - * Forwards all passed parameters to {@link fseek()} - * - * @throws IcingaException In case of an error - */ - protected function fseek() - { - if (call_user_func_array('fseek', func_get_args()) === -1) { - $this->reportFunctionCallError('fseek', func_get_args()); - } - } - - /** - * Forwards all passed parameters to {@link fwrite()} - * - * @throws IcingaException In case of an error - */ - protected function fwrite() - { - if (call_user_func_array('fwrite', func_get_args()) === 0) { - $this->reportFunctionCallError('fwrite', func_get_args()); - } - } - - /** - * Throw an exception telling that the given function call didn't succeed - * - * @param string $functionName - * @param array $passedArgs - * - * @throws IcingaException unconditional - */ - protected function reportFunctionCallError($functionName, array $passedArgs) - { - $args = array(); - foreach ($passedArgs as $arg) { - $args[] = print_r($arg, true); - } - throw new IcingaException('Couldn\'t ' . $functionName . '(' . implode(', ', $args) . ')'); + parent::__construct($this->assertSuccessfulFunctionCall('fopen', array('php://temp', 'w+b'))); } } diff --git a/library/Icinga/Util/StreamWrapper.php b/library/Icinga/Util/StreamWrapper.php new file mode 100644 index 000000000..f86bfd30c --- /dev/null +++ b/library/Icinga/Util/StreamWrapper.php @@ -0,0 +1,209 @@ +handle = $handle; + } + + /** + * @see {@link StreamInterface::__toString()} + */ + public function __toString() + { + try { + $this->rewind(); + return $this->getContents(); + } catch (Exception $e) { + return (string) $e; + } + } + + /** + * @see {@link StreamInterface::close()} + */ + public function close() + { + $this->assertSuccessfulFunctionCall('fclose', array($this->handle)); + } + + /** + * @see {@link StreamInterface::detach()} + */ + public function detach() + { + $handle = $this->handle; + $this->handle = null; + return $handle; + } + + /** + * @see {@link StreamInterface::getSize()} + */ + public function getSize() + { + $stats = $this->assertSuccessfulFunctionCall('fstat', array($this->handle)); + return $stats['size']; + } + + /** + * @see {@link StreamInterface::tell()} + */ + public function tell() + { + return $this->assertSuccessfulFunctionCall('ftell', array($this->handle)); + } + + /** + * @see {@link StreamInterface::eof()} + */ + public function eof() + { + return feof($this->handle); + } + + /** + * @see {@link StreamInterface::isSeekable()} + */ + public function isSeekable() + { + return $this->getMetadata('seekable'); + } + + /** + * @see {@link StreamInterface::seek()} + */ + public function seek($offset, $whence = SEEK_SET) + { + $this->assertSuccessfulFunctionCall('fseek', array($this->handle, $offset, $whence), function ($value) { + return $value !== -1; + }); + } + + /** + * @see {@link StreamInterface::rewind()} + */ + public function rewind() + { + $this->assertSuccessfulFunctionCall('rewind', array($this->handle)); + } + + /** + * @see {@link StreamInterface::isWritable()} + */ + public function isWritable() + { + return ! preg_match('/^r(?!\+)/', $this->getMetadata('mode')); + } + + /** + * @see {@link StreamInterface::write()} + */ + public function write($string) + { + return $this->assertSuccessfulFunctionCall('fwrite', array($this->handle, $string)); + } + + /** + * @see {@link StreamInterface::isReadable()} + */ + public function isReadable() + { + return preg_match('/[r|+]/', $this->getMetadata('mode')); + } + + /** + * @see {@link StreamInterface::read()} + */ + public function read($length) + { + return $this->assertSuccessfulFunctionCall('fread', array($this->handle, $length)); + } + + /** + * @see {@link StreamInterface::getContents()} + */ + public function getContents() + { + return $this->assertSuccessfulFunctionCall('stream_get_contents', array($this->handle)); + } + + /** + * @see {@link StreamInterface::getMetadata()} + */ + public function getMetadata($key = null) + { + $result = stream_get_meta_data($this->handle); + return isset($result[$key]) ? $result[$key] : $result; + } + + /** + * Pass the data stored in the buffer to the user agent (as with {@link fpassthru()}) + */ + public function fpassthru() + { + // fpassthru() returns the amount of bytes passed so we could + // perform an error check, but throwing exceptions here makes + // no sense as we're already writing the response body + fpassthru($this->handle); + } + + /** + * Call a function as with {@link call_user_func_array()} + * + * @param string $functionName + * @param array $args + * @param callable $resultValidator + * + * @return mixed + * + * @throws RuntimeException If the function call didn't succeed + */ + protected function assertSuccessfulFunctionCall($functionName, array $args, $resultValidator = null) + { + $result = call_user_func_array($functionName, $args); + if ($resultValidator === null ? $result === false : ! call_user_func($resultValidator, $result)) { + throw new RuntimeException( + 'Error: ' . $functionName . '(' . implode(', ', array_map(array($this, 'represent'), $args)) + . ') = ' . $this->represent($result) + ); + } + + return $result; + } + + /** + * Return a value's string representation + * + * @param mixed $value + * + * @return string + */ + protected function represent($value) + { + return is_scalar($value) ? var_export($value, true) : print_r($value, true); + } +}