web-dev-qa-db-ja.com

リソースからストリームを作成する

fopen 関数を使用して、ファイル名(実際のファイル、またはURL)から PHPストリーム を作成できることを知っています。

$stream = fopen('php://temp', 'r');

結果のストリーム($stream)は、URLから作成されたタイプ "stream"resourceですphp://temp

しかし、リソースから上記のようなストリームをどのように作成できますか?


なぜ私はこれを尋ねるのですか?

私は PSR-7 ライブラリに取り組んでおり、PSR-7 StreamInterfaceStreamクラスで実装しました。 Streamインスタンスを作成するために、StreamFactoryも実装することにしました。そのインターフェースStreamFactoryInterfacePSR-17(HTTP Factories) 提案で定義されています。

StreamFactoryInterfaceは、createStreamFromResourceという名前のメソッドを定義します。これは、その公式コメントに準拠しており、次の条件を満たす必要があります。

既存のリソースから新しいストリームを作成します。

ストリームは読み取り可能でなければならず、書き込み可能である必要があります。

したがって、ファクトリメソッドはリソースを引数として受け取ります。そして、その具体的な実装では、新しいStreamオブジェクトが作成されます-これも引数としてリソースを受け取ります。

これが問題です:

簡単にするために、Streamクラスがストリームでのみ機能するとします。タイプ "stream"のリソースを使用します。タイプ "stream"ではないリソースを受信した場合、それは拒否されます。

では、createStreamFromResourceのリソース引数がまだタイプ "stream"のリソースでない場合はどうなるでしょうか。どうすればストリームに変換できますか。タイプ "stream"のリソースに入れて、それをさらに渡すことができるように、新しいStreamオブジェクトを作成するための呼び出しにそれを渡しますか?このタスクを達成する方法(PHPメソッド、関数、またはキャスト関数)はありますか?

注:

  • わかりやすくするために、ストリームの作成方法の完全な例(testStream.php)を用意しました。 Streamインスタンス、3つの方法:直接1回、およびストリームファクトリを使用して2回。
  • また、ファクトリーインターフェースの具体的な実装、クラスStreamFactory、メソッドcreateStreamFromResourceも投稿します。このメソッドの呼び出しは、testStream.phpでストリームを作成する4番目の方法です。
  • さらに、クラスStreamResponseを提示して、必要に応じてすべてを直接テストできるようにします。 2つのクラスは、私の実際のコードを非常に単純化したバージョンです。
  • 私のコードでは、2つの質問の場所に "@ asking"のタグを付けました。

時間と忍耐をありがとうございました!


testStream.php(テストページ)

<?php

use Tests\Stream;
use Tests\Response;
use Tests\StreamFactory;

/*
 * ================================================
 * Option 1: Create a stream by a stream name
 * (like "php://temp") with read and write rights.
 * ================================================
 */
$stream = new Stream('php://temp', 'w+b');

$response = new Response($stream);
$response->getBody()->write(
        'Stream 1: Created directly.<br/><br/>'
);
echo $response->getBody();

/*
 * ================================================
 * Option 2: Create a stream by a stream name
 * (like "php://temp"), using a stream factory.
 * ================================================
 */
$streamFactory = new StreamFactory();
$stream = $streamFactory->createStreamFromFile('php://temp', 'w+b');

$response = new Response($stream);
$response->getBody()->write(
        'Stream 2: Created by a stream name, with a stream factory.<br/><br/>'
);
echo $response->getBody();

/*
 * ================================================
 * Option 3: Create a stream from a string, using a
 * stream factory.
 * ================================================
 */
$streamFactory = new StreamFactory();
$stream = $streamFactory->createStream(
        'Stream 3: Created from a string, with a stream factory.<br/><br/>'
);

$response = new Response($stream);
echo $response->getBody();

/*
 * ================================================
 * Option 4: Create a stream from an existing
 * resource, using a stream factory.
 * ================================================
 * 
 * @asking How can I create a stream by calling the
 * the factory method ServerFactory::createStreamFromResource
 * with a resource which is not of type "stream"?
 */
//...

StreamFactoryクラス(私が持っているので、単純化されていません)

<?php

namespace Tests;

use Tests\Stream;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\StreamFactoryInterface;

class StreamFactory implements StreamFactoryInterface {

    /**
     * Create a new stream from an existing resource.
     *
     * The stream MUST be readable and may be writable.
     *
     * @param resource $resource
     *
     * @return StreamInterface
     * @throws \InvalidArgumentException
     */
    public function createStreamFromResource($resource) {
        /*
         * @asking What if $resource is not already a resource of type *"stream"*? 
         * How can I transform it into a stream, e.g. into a resource of type *"stream"*, 
         * so that I can pass it further, to the call for creating a new `Stream` object 
         * with it? Is there a way (a PHP method, a function, or maybe a casting function) 
         * of achieving this task?
         */
         //...

        return new Stream($resource, 'w+b');
    }

    /**
     * Create a new stream from a string.
     *
     * The stream SHOULD be created with a temporary resource.
     *
     * @param string $content
     *
     * @return StreamInterface
     * @throws \InvalidArgumentException
     */
    public function createStream($content = '') {
        if (!isset($content) || !is_string($content)) {
            throw new \InvalidArgumentException('For creating a stream, a content string must be provided!');
        }

        $stream = $this->createStreamFromFile('php://temp', 'w+b');

        $stream->write($content);

        return $stream;
    }

    /**
     * Create a stream from an existing file.
     *
     * The file MUST be opened using the given mode, which may be any mode
     * supported by the `fopen` function.
     *
     * The `$filename` MAY be any string supported by `fopen()`.
     *
     * @param string $filename
     * @param string $mode
     *
     * @return StreamInterface
     */
    public function createStreamFromFile($filename, $mode = 'r') {
        return new Stream($filename, $mode);
    }

}

Streamクラス(非常に単純化)

<?php

namespace Tests;

use Psr\Http\Message\StreamInterface;

class Stream implements StreamInterface {

    /**
     * Stream (resource).
     *
     * @var resource
     */
    private $stream;

    /**
     *
     * @param string|resource $stream Stream name, or resource.
     * @param string $accessMode (optional) Access mode.
     * @throws \InvalidArgumentException
     */
    public function __construct($stream, string $accessMode = 'r') {
        if (
                !isset($stream) ||
                (!is_string($stream) && !is_resource($stream))
        ) {
            throw new \InvalidArgumentException(
                'The provided stream must be a filename, or an opened resource of type "stream"!'
            );
        }

        if (is_string($stream)) {
            $this->stream = fopen($stream, $accessMode);
        } elseif (is_resource($stream)) {
            if ('stream' !== get_resource_type($stream)) {
                throw new \InvalidArgumentException('The provided resource must be of type "stream"!');
            }

            $this->stream = $stream;
        }
    }

    /**
     * Write data to the stream.
     *
     * @param string $string The string that is to be written.
     * @return int Returns the number of bytes written to the stream.
     * @throws \RuntimeException on failure.
     */
    public function write($string) {
        return fwrite($this->stream, $string);
    }

    /**
     * Reads all data from the stream into a string, from the beginning to end.
     *
     * @return string
     */
    public function __toString() {
        try {
            // Rewind the stream.
            fseek($this->stream, 0);

            // Get the stream contents as string.
            $contents = stream_get_contents($this->stream);

            return $contents;
        } catch (\RuntimeException $exc) {
            return '';
        }
    }

    public function close() {}
    public function detach() {}
    public function eof() {}
    public function getContents() {}
    public function getMetadata($key = null) {}
    public function getSize() {}
    public function isReadable() {}
    public function isSeekable() {}
    public function isWritable() {}
    public function read($length) {}
    public function rewind() {}
    public function seek($offset, $whence = SEEK_SET) {}
    public function tell() {}

}

応答クラス(非常に簡略化)

<?php

namespace Tests;

use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\ResponseInterface;

class Response implements ResponseInterface {

    /**
     *
     * @param StreamInterface $body Message body.
     */
    public function __construct(StreamInterface $body) {
        $this->body = $body;
    }

    /**
     * Gets the body of the message.
     *
     * @return StreamInterface Returns the body as a stream.
     */
    public function getBody() {
        return $this->body;
    }

    public function getHeader($name) {}
    public function getHeaderLine($name) {}
    public function getHeaders() {}
    public function getProtocolVersion() {}
    public function hasHeader($name) {}
    public function withAddedHeader($name, $value) {}
    public function withBody(StreamInterface $body) {}
    public function withHeader($name, $value) {}
    public function withProtocolVersion($version) {}
    public function withoutHeader($name) {}
    public function getReasonPhrase() {}
    public function getStatusCode() {}
    public function withStatus($code, $reasonPhrase = '') {}

}
10
dakis

渡された引数の処理方法は、最終的な実装によって異なります。コードがストリーム引数を期待している場合は、そのようなものを検出しないときに停止する必要があります。ただし、コードが問題を処理することが期待される場合は、ストリームの作成を試みることができます。

編集

最初から取得できませんでしたが、リソース変数を変換できるかどうかが問題だったようです。 documentation によると、これは不可能であり、意味がありません。

1
Emil

PSR-7には非常に優れた実装がかなりありますStreamInterface最初に検討することをお勧めします。どのような検証とロジックを実行する必要があるかについて、いくつかのアイデアを得るかもしれません。

更新:これらすべてのリンクを調べた後、私はあなたの現在のコードにいくつかの問題を見つけました:

  • コンストラクタでリソースタイプを確認する必要があります。たとえば、MySQLリソースである可能性があり、それに書き込みたくないとします。

    public function __construct($stream, string $accessMode = 'r') {
    
        if (is_string($stream)) {
            $stream = fopen($stream, $accessMode);
        }
    
        if (! is_resource($stream) || 'stream' !== get_resource_type($stream)) {
            throw new InvalidArgumentException(
                'Invalid stream provided; must be a string stream identifier or stream resource'
            );
        }
    
        $this->stream = $stream;
    }
    
  • ストリームに書き込むときは、ストリームが実際に書き込み可能かどうかを確認してください。最初にisWritableメソッドを実装し、write関数で呼び出す必要があります。この例はzend-diactorosライブラリからの抜粋です。

    public function isWritable()
    {
        if (! $this->resource) {
            return false;
        }
        $meta = stream_get_meta_data($this->resource);
        $mode = $meta['mode'];
        return (
            strstr($mode, 'x')
            || strstr($mode, 'w')
            || strstr($mode, 'c')
            || strstr($mode, 'a')
            || strstr($mode, '+')
        );
    }
    
  • 最初にreadおよびseekを実装する必要があるisSeekableおよびisReadable関数と同じです。

  • __toStringは、ストリームが読み取り可能でシーク可能かどうかも確認する必要があります。

    public function __toString()
    {
        if (! $this->isReadable()) {
            return '';
        }
        try {
            if ($this->isSeekable()) {
                $this->rewind();
            }
            return $this->getContents();
        } catch (RuntimeException $e) {
            return '';
        }
    } 
    

お役に立てれば。新しいライブラリで頑張ってください。

1
zstate

任意の方法で実装できますが、このメソッドは基本的に、事前に生成されたリソースの単なるラッパーです。

ほとんどの場合、ストリームは文字列とおそらく設定/オプションの配列を受け取り、情報からストリームを作成します(途中のどこかでfopen('http://...')を使用する可能性があります)。

CreateStreamFromResource($ resource)は、事前に生成されたリソース(たとえば、fopenを実行するためのデータではなく、fopenからの戻りリソース値)を取り込みます。

class Stream implements StreamInterface {

    // ...

    public function __construct($url, $opt = null) {

        // ...

        if( is_resource( $url ) ) {

            /*
             * Check that the $resource is a valid resource
             * (e.g. an http request from an fopen call vs a mysql resource.)
             * or possibly a stream context that still needs to create a
             * request...
             */

            if( !$isValid ) {
                return false;
            }

            $this->resource = $resource;

        } else {

            // ...

            $this->resource = fopen($url, $modifiedOpt);

            // ...

        }

    }

    // ...

    /* createStreamFromResource would call Stream::fromResource($r)
     * or possibly Stream($resource) directly, your call.
     */
    static function fromResource($resource) {
        return new static($resource);
    }

}

ファクトリメソッドは、次のように単純なものにすることができます。

public function createStreamFromResource($resource) {
    return Stream::fromResource($resource);
}
1
Jim