Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
66.67% covered (warning)
66.67%
14 / 21
CRAP
90.91% covered (success)
90.91%
100 / 110
Stream
0.00% covered (danger)
0.00%
0 / 1
66.67% covered (warning)
66.67%
14 / 21
58.36
90.91% covered (success)
90.91%
100 / 110
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 __destruct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 __toString
0.00% covered (danger)
0.00%
0 / 1
3.33
66.67% covered (warning)
66.67%
4 / 6
 close
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
5 / 5
 detach
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 attach
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 getSize
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 tell
0.00% covered (danger)
0.00%
0 / 1
3.04
83.33% covered (success)
83.33%
5 / 6
 eof
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
 isSeekable
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 seek
0.00% covered (danger)
0.00%
0 / 1
4.25
75.00% covered (success)
75.00%
6 / 8
 rewind
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 isWritable
100.00% covered (success)
100.00%
1 / 1
6
100.00% covered (success)
100.00%
9 / 9
 write
0.00% covered (danger)
0.00%
0 / 1
4.03
87.50% covered (success)
87.50%
7 / 8
 isReadable
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
5 / 5
 read
0.00% covered (danger)
0.00%
0 / 1
4.25
75.00% covered (success)
75.00%
6 / 8
 getContents
0.00% covered (danger)
0.00%
0 / 1
3.04
83.33% covered (success)
83.33%
5 / 6
 getMetaData
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
6 / 6
 setStream
0.00% covered (danger)
0.00%
0 / 1
5.01
93.33% covered (success)
93.33%
14 / 15
 isAvailableStream
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
3 / 3
 closeTmpStream
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
<?php
namespace Puyo\Psr7;
/**
 * ストリームを抽象化したもの
 *
 * PSR-7 の StreamInterface の実装。
 * HTTPに限らず利用できるが、現状はバイナリにしか対応していない。
 */
class Stream implements \Psr\Http\Message\StreamInterface
{
    /** @var resource */
    protected $fp;
    /**
     * ファイルのパスから生成した場合に内部で開くためのStream
     * @var resource
     */
    protected $tmpFp;
    /**
     * ファイルのフルパスもしくはresourceから生成する
     * @param resource|string $fileOrStream
     * @param string $mode
     */
    public function __construct($fileOrStream, $mode='rb') {
        $this->setStream($fileOrStream, $mode);
    }
    /**
     * ファイルパスから生成した場合の一時Streamを解放する
     */
    public function __destruct() {
        $this->closeTmpStream();
    }
    /**
     * すべてのデータを文字列で取得する
     * @return string content
     */
    public function __toString() {
        if(!$this->isReadable()) {
            return '';
        }
        try {
            $this->rewind();
            return $this->getContents();
        } catch(\RuntimeException $e) {
            return '';
        }
    }
    /**
     * {@inheritdoc}
     */
    public function close() {
        if(!$this->fp) {
            return;
        }
        $fp = $this->detach();
        fclose($fp);
    }
    /**
     * @return resource
     */
    public function detach() {
        $fp = $this->fp;
        $this->fp = null;
        return $fp;
    }
    /**
     * streamもしくはファイルのフルパスからstreamを割り当てる
     * @param string|resource $fileOrStream
     * @param string $mode
     */
    public function attach($fileOrStream, $mode='rb') {
        $this->closeTmpStream();
        $this->setStream($fileOrStream, $mode);
    }
    /**
     * データサイズを返す
     * @return int|null 閉じられている場合はnull
     */
    public function getSize() {
        if(null === $this->fp) {
            return null;
        }
        $stats = fstat($this->fp);
        return $stats['size'];
    }
    /**
     * 現在の書き込みのポインタ位置を返す
     * {@inheritdoc}
     * @return int
     */
    public function tell() {
        if(null === $this->fp) {
            throw new \RuntimeException('No resource available. Cannot tell position');
        }
        $result = ftell($this->fp);
        if(!is_int($result)) {
            throw new \RuntimeException('Cannot tell position');
        }
        return $result;
    }
    /**
     * {@inheritdoc}
     * @return bool 終端、もしくは未オープンの場合
     */
    public function eof() {
        if(null === $this->fp) {
            return true;
        }
        return feof($this->fp);
    }
    /**
     * {@inheritdoc}
     * @return bool
     */
    public function isSeekable() {
        if(!$this->isAvailableStream($this->fp)) {
            return false;
        }
        $meta = stream_get_meta_data($this->fp);
        return $meta['seekable'];
    }
    /**
     * {@inheritdoc}
     * @param int $offset
     * @param int $whence SEEK_* constant
     * @return void
     * @throws \RuntimeException シーク失敗時
     */
    public function seek($offset, $whence=SEEK_SET) {
        if (null === $this->fp) {
            throw new \RuntimeException('No resource available. Cannot seek position');
        }
        if (!$this->isSeekable()) {
            throw new \RuntimeException('Stream is not seekable');
        }
        $result = fseek($this->fp, $offset, $whence);
        if (0 !== $result) {
            throw new \RuntimeException('Error seeking within stream');
        }
    }
    /**
     * ポインタを先頭に戻す
     * @return void
     */
    public function rewind() {
        return $this->seek(0);
    }
    /**
     * {@inheritdoc}
     * @return bool
     */
    public function isWritable() {
        if(null === $this->fp) {
            return false;
        }
        $meta = stream_get_meta_data($this->fp);
        $mode = $meta['mode'];
        return (
            strstr($mode, 'x')
            || strstr($mode, 'w')
            || strstr($mode, 'c')
            || strstr($mode, 'a')
            || strstr($mode, '+')
        );
    }
    /**
     * 書き込む
     * @param string $string
     * @return int 書き込みバイト数
     */
    public function write($string) {
        if(null === $this->fp) {
            throw new \RuntimeException('No resource available. Cannot write');
        }
        if (!$this->isWritable()) {
            throw new \RuntimeException('Stream is not writable');
        }
        $result = fwrite($this->fp, $string);
        if(false === $result) {
            throw new \RuntimeException('Error writing to stream');
        }
        return $result;
    }
    /**
     * {@inheritdoc}
     * @return bool
     */
    public function isReadable() {
        if(null === $this->fp) {
            return false;
        }
        $meta = stream_get_meta_data($this->fp);
        $mode = $meta['mode'];
        return (
            strstr($mode, 'r') || strstr($mode, '+')
        );
    }
    /**
     * 読み込み
     * @param int $length bytes
     * @return string 読み込んだデータ
     */
    public function read($length) {
        if (null === $this->fp) {
            throw new \RuntimeException('No resource available. Cannot read');
        }
        if (!$this->isReadable()) {
            throw new \RuntimeException('Stream is not readable');
        }
        $result = fread($this->fp, $length);
        if (false === $result) {
            throw new \RuntimeException('Error reading from stream');
        }
        return $result;
    }
    /**
     * 残りのストリームすべてを返す
     * @return string
     */
    public function getContents() {
        if (!$this->isReadable()) {
            throw new \RuntimeException('Stream is not readable');
        }
        $result = stream_get_contents($this->fp);
        if (false === $result) {
            throw new \RuntimeException('Error reading from stream');
        }
        return $result;
    }
    /**
     * {@inheritdoc}
     * @param string $key (optional)
     * @return array|null
     */
    public function getMetaData($key=null) {
        $metadata = stream_get_meta_data($this->fp);
        if($key === null) {
            return $metadata;
        }
        if (!array_key_exists($key, $metadata)) {
            return null;
        }
        return $metadata[$key];
    }
    private function setStream($fileOrStream, $mode='rb') {
        $error = null;
        $fp = $tmpFp = null;
        if(is_string($fileOrStream)) {
            set_error_handler(function ($e) use (&$error) {
                $error = $e;
            }, E_WARNING);
            $tmpFp = fopen($fileOrStream, $mode);
            restore_error_handler();
            if($error !== null) {
                throw new \InvalidArgumentException('Invalid stream reference provided');
            }
        } elseif($this->isAvailableStream($fileOrStream)) {
            $fp = $fileOrStream;
        }
        $this->fp = $fp;
        if($tmpFp !== null) {
            $this->fp = $this->tmpFp = $tmpFp;
        }
    }
    /**
     * @param resource $fp
     * @return bool
     */
    private function isAvailableStream($fp) {
        if(is_resource($fp) && 'stream' === get_resource_type($fp)) {
            return true;
        }
        return false;
    }
    private function closeTmpStream() {
        if($this->isAvailableStream($this->tmpFp)) {
            fclose($this->tmpFp);
        }
    }
}