从资源创建流
Create a stream from a resource
我知道我可以创建一个 PHP stream from a filename (a real one, or an URL), by using the fopen 函数:
$stream = fopen('php://temp', 'r');
结果流 ($stream
) 然后是 资源 类型 “流”,从 URL php://temp
.
但是我如何从资源创建像上面这样的流?
我为什么要问这个?
我正在研究 PSR-7 library and I implemented the PSR-7 StreamInterface with a Stream
class. In order to create Stream
instances, I decided to implement a StreamFactory
too. Its interface, StreamFactoryInterface
, is defined in PSR-17: HTTP Factories。
StreamFactoryInterface
定义了一个名为 createStreamFromResource
的方法,根据其官方评论,该方法应该:
Create a new stream from an existing resource.
The stream MUST be readable and may be writable.
因此工厂方法接收资源作为参数。并且,在其具体实现中,创建了一个新的 Stream
对象——它也接收一个资源作为参数。
问题是:
为了简单起见,假设 Stream
class 仅适用于流,例如资源类型为 "stream"。如果它收到的资源不是 "stream" 类型,它会拒绝它。
那么,如果 createStreamFromResource
的资源参数还不是 "stream" 类型的资源怎么办?我怎样才能把它变成一个流,例如放入类型为 "stream" 的资源中,以便我可以将它进一步传递给使用它创建新的 Stream
对象的调用?有没有办法(一个 PHP 方法、一个函数,或者一个转换函数)来完成这个任务?
备注:
- 为清楚起见,我准备了一个完整的示例 (
testStream.php
) 来说明如何创建流,例如一个 Stream
实例,以三种方式:一次直接,两次使用流工厂。
- 我也post工厂接口的具体实现:class
StreamFactory
和方法createStreamFromResource
。调用此方法应该是我在 testStream.php
. 中创建流的第四种方式
- 此外,我提供了 classes
Stream
和 Response
,以便您可以直接测试所有内容,如果您愿意的话。这两个 classes 是我的真实代码的一个非常简化的版本。
- 在我的代码中,我用 "@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"?
*/
//...
StreamFactoryclass(因为我有,所以没有简化):
<?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);
}
}
流class(非常简化):
<?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() {}
}
响应class(非常简单):
<?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 = '') {}
}
您可以按照自己喜欢的任何方式实现它,但此方法本质上只是 pre-generated 资源的包装器。
在大多数情况下,您的 Stream 可能会接收一个字符串,也可能是一个 settings/options 数组,并根据信息创建一个流(可能在某个地方使用 fopen('http://...')
。
createStreamFromResource($resource) 将接收 pre-generated 资源(例如 fopen
中的 return 资源值,而不是执行 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);
}
有很多非常好的 PSR-7 实现 StreamInterface
我建议先看一下。您可能会对需要执行的验证和逻辑类型有所了解。
- guzzle/psr7 - PSR-7 的 Guzzle 实现
StreamInterface
- reactphp/stream - This one doesn't implement PSR-7, but the guys put a lot of thoughts into their implementation and code is very well documented. Look at ReadableResourceStream and WritableResourceStream.
- zendframework/zend-diactoros
- slimphp/Slim
更新:
查看所有这些链接后,我发现了您当前代码的一些问题:
您必须在构造函数中检查资源类型。例如,它可能是 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 '';
}
}
希望这对您有所帮助。祝你的新图书馆好运。
您如何处理传递的参数取决于您的最终实现。如果您的代码需要一个流参数,那么它应该在没有检测到这样的东西时停止。但是,如果您的代码有望处理该问题,那么您可以尝试创建一个流。
编辑
一开始没弄明白,但看起来问题是是否可以转换资源变量。根据 documentation 这是不可能的,没有意义。
我知道我可以创建一个 PHP stream from a filename (a real one, or an URL), by using the fopen 函数:
$stream = fopen('php://temp', 'r');
结果流 ($stream
) 然后是 资源 类型 “流”,从 URL php://temp
.
但是我如何从资源创建像上面这样的流?
我为什么要问这个?
我正在研究 PSR-7 library and I implemented the PSR-7 StreamInterface with a Stream
class. In order to create Stream
instances, I decided to implement a StreamFactory
too. Its interface, StreamFactoryInterface
, is defined in PSR-17: HTTP Factories。
StreamFactoryInterface
定义了一个名为 createStreamFromResource
的方法,根据其官方评论,该方法应该:
Create a new stream from an existing resource.
The stream MUST be readable and may be writable.
因此工厂方法接收资源作为参数。并且,在其具体实现中,创建了一个新的 Stream
对象——它也接收一个资源作为参数。
问题是:
为了简单起见,假设 Stream
class 仅适用于流,例如资源类型为 "stream"。如果它收到的资源不是 "stream" 类型,它会拒绝它。
那么,如果 createStreamFromResource
的资源参数还不是 "stream" 类型的资源怎么办?我怎样才能把它变成一个流,例如放入类型为 "stream" 的资源中,以便我可以将它进一步传递给使用它创建新的 Stream
对象的调用?有没有办法(一个 PHP 方法、一个函数,或者一个转换函数)来完成这个任务?
备注:
- 为清楚起见,我准备了一个完整的示例 (
testStream.php
) 来说明如何创建流,例如一个Stream
实例,以三种方式:一次直接,两次使用流工厂。 - 我也post工厂接口的具体实现:class
StreamFactory
和方法createStreamFromResource
。调用此方法应该是我在testStream.php
. 中创建流的第四种方式
- 此外,我提供了 classes
Stream
和Response
,以便您可以直接测试所有内容,如果您愿意的话。这两个 classes 是我的真实代码的一个非常简化的版本。 - 在我的代码中,我用 "@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"?
*/
//...
StreamFactoryclass(因为我有,所以没有简化):
<?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);
}
}
流class(非常简化):
<?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() {}
}
响应class(非常简单):
<?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 = '') {}
}
您可以按照自己喜欢的任何方式实现它,但此方法本质上只是 pre-generated 资源的包装器。
在大多数情况下,您的 Stream 可能会接收一个字符串,也可能是一个 settings/options 数组,并根据信息创建一个流(可能在某个地方使用 fopen('http://...')
。
createStreamFromResource($resource) 将接收 pre-generated 资源(例如 fopen
中的 return 资源值,而不是执行 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);
}
有很多非常好的 PSR-7 实现 StreamInterface
我建议先看一下。您可能会对需要执行的验证和逻辑类型有所了解。
- guzzle/psr7 - PSR-7 的 Guzzle 实现
StreamInterface
- reactphp/stream - This one doesn't implement PSR-7, but the guys put a lot of thoughts into their implementation and code is very well documented. Look at ReadableResourceStream and WritableResourceStream.
- zendframework/zend-diactoros
- slimphp/Slim
更新: 查看所有这些链接后,我发现了您当前代码的一些问题:
您必须在构造函数中检查资源类型。例如,它可能是 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 ''; } }
希望这对您有所帮助。祝你的新图书馆好运。
您如何处理传递的参数取决于您的最终实现。如果您的代码需要一个流参数,那么它应该在没有检测到这样的东西时停止。但是,如果您的代码有望处理该问题,那么您可以尝试创建一个流。
编辑
一开始没弄明白,但看起来问题是是否可以转换资源变量。根据 documentation 这是不可能的,没有意义。