如何从资源创建PHP流?PSR-7 Stream类开发技术问询
How to Create a PSR-7 Stream from an Existing PHP Resource
Great question! When building a PSR-7 library, wrapping an existing PHP stream resource into your custom Stream class (that implements StreamInterface) is a common need. The core idea is to delegate all the PSR-7 stream methods to the underlying PHP resource while handling validation and error checking properly. Here's a step-by-step guide to implement this:
Core Approach
To turn an existing resource into a PSR-7 compliant stream:
- Validate the resource: Ensure it’s a valid PHP stream resource before proceeding.
- Extract metadata: Use
stream_get_meta_data()to pull critical info like the stream’s mode, seekability, and URI. - Delegate methods: Implement every
StreamInterfacemethod by calling the corresponding PHP stream function (e.g.,fread()forread(),fseek()forseek()).
Example Implementation
Here’s how to extend your existing Stream class with a static factory method to wrap resources, plus key method implementations:
use Psr\Http\Message\StreamInterface; class Stream implements StreamInterface { /** @var resource|null The underlying PHP stream resource */ private $resource; /** @var array Stream metadata (from stream_get_meta_data) */ private $metadata; // Your existing constructor (if any) can remain, or adjust to accept resources directly private function __construct() {} /** * Create a Stream instance from an existing PHP stream resource. * * @param resource $resource A valid PHP stream resource. * @return self * @throws \InvalidArgumentException If the input isn't a valid stream resource. */ public static function fromResource($resource): self { // Validate the input is a stream resource if (!is_resource($resource) || get_resource_type($resource) !== 'stream') { throw new \InvalidArgumentException('Provided value must be a valid PHP stream resource.'); } $stream = new self(); $stream->resource = $resource; $stream->metadata = stream_get_meta_data($resource); return $stream; } // --- StreamInterface Method Implementations --- public function read(int $length): string { if (!$this->resource) { throw new \RuntimeException('Cannot read from a closed stream.'); } $data = fread($this->resource, $length); if ($data === false) { throw new \RuntimeException('Failed to read data from the stream.'); } return $data; } public function write(string $string): int { if (!$this->resource) { throw new \RuntimeException('Cannot write to a closed stream.'); } $bytesWritten = fwrite($this->resource, $string); if ($bytesWritten === false) { throw new \RuntimeException('Failed to write data to the stream.'); } return $bytesWritten; } public function seek(int $offset, int $whence = SEEK_SET): void { if (!$this->resource) { throw new \RuntimeException('Cannot seek a closed stream.'); } if (!$this->isSeekable()) { throw new \RuntimeException('This stream is not seekable.'); } $result = fseek($this->resource, $offset, $whence); if ($result !== 0) { throw new \RuntimeException('Failed to seek to the specified position.'); } } public function tell(): int { if (!$this->resource) { throw new \RuntimeException('Cannot get position of a closed stream.'); } $position = ftell($this->resource); if ($position === false) { throw new \RuntimeException('Failed to retrieve stream position.'); } return $position; } public function eof(): bool { return !$this->resource || feof($this->resource); } public function close(): void { if ($this->resource) { fclose($this->resource); $this->resource = null; $this->metadata = []; } } public function isReadable(): bool { if (!$this->resource) return false; $mode = $this->metadata['mode'] ?? ''; return strpos($mode, 'r') !== false || strpos($mode, '+') !== false; } public function isWritable(): bool { if (!$this->resource) return false; $mode = $this->metadata['mode'] ?? ''; return strpos($mode, 'w') !== false || strpos($mode, 'a') !== false || strpos($mode, '+') !== false; } public function isSeekable(): bool { return isset($this->metadata['seekable']) && $this->metadata['seekable']; } public function getMetadata($key = null) { if ($key === null) return $this->metadata; return $this->metadata[$key] ?? null; } // Implement remaining StreamInterface methods (getContents(), getSize(), etc.) // For getSize(), you can use fstat($this->resource)['size'] if available public function getContents(): string { if (!$this->resource) throw new \RuntimeException('Cannot read contents of a closed stream.'); $contents = stream_get_contents($this->resource); if ($contents === false) throw new \RuntimeException('Failed to read stream contents.'); return $contents; } // Add a destructor to auto-close the resource when the Stream instance is garbage-collected public function __destruct() { $this->close(); } }
How to Use It
Once you have this factory method, wrapping an existing resource is straightforward:
// Wrap a local file stream $fileResource = fopen('/path/to/your/file.txt', 'r'); $fileStream = Stream::fromResource($fileResource); // Wrap a php://temp stream (like your example) $tempResource = fopen('php://temp', 'r+'); $tempStream = Stream::fromResource($tempResource);
Key Considerations
- Resource Cleanup: The
__destruct()method ensures the underlying resource is closed when the Stream instance is no longer needed, preventing resource leaks. - Error Handling: All methods throw appropriate exceptions as required by PSR-7, making your stream implementation robust.
- Metadata Accuracy: Using
stream_get_meta_data()ensures your stream correctly reports its capabilities (readable, writable, seekable) based on the original resource’s mode.
内容的提问来源于stack exchange,提问作者user9455607
相关产品推荐
相关产品推荐

