A 1913 engineering drawing of a steel discharge manifold, showing a single intake pipe splitting at a T-junction into three separate branches, each ending in its own flanged connector
PHP

PHP Stream Wrappers: Mastering I/O Abstraction and Custom Protocols

•9 min read•By Joseph Edmonds•Image: U.S. Reclamation Service / HAER, public domain, via Wikimedia Commons

PHP's stream wrapper system provides a powerful abstraction layer for I/O operations, enabling consistent access to files, URLs, compressed data, and custom protocols through familiar functions like fopen() and file_get_contents(). This guide explores built-in wrappers, their practical applications, and how to implement custom stream handlers for specialised data sources.

Understanding Stream Wrappers

PHP streams provide a unified interface for various I/O operations. Each stream is identified by a scheme and target: scheme://target. The scheme determines which wrapper handles the stream, whilst the target specifies what to access.

<?php
// Built-in wrapper examples
$fileHandle = fopen('file:///path/to/file.txt', 'r');
$webContent = file_get_contents('https://api.example.com/data');
$tempStream = fopen('php://temp', 'w+');
$dataStream = fopen('data://text/plain;base64,SGVsbG8gV29ybGQ=', 'r');

// List all registered wrappers
$wrappers = stream_get_wrappers();
print_r($wrappers);
?>

The stream_get_wrappers() function reveals all available protocols, typically including: file, http, https, ftp, php, compress.zlib, data, phar, and zip.

File System Wrapper (file://)

The file:// wrapper is the default handler for local filesystem access, and when no scheme is specified PHP simply assumes it, supporting all standard filesystem operations and metadata retrieval.

<?php
// These are equivalent
$handle1 = fopen('/var/log/app.log', 'r');
$handle2 = fopen('file:///var/log/app.log', 'r');

// Reading with context options for large files
$context = stream_context_create([
    'file' => [
        'chunk_size' => 8192  // Read in 8KB chunks
    ]
]);

$logData = file_get_contents('/var/log/app.log', false, $context);

// Checking file permissions before access
if (is_readable('/etc/passwd')) {
    $passwd = file('/etc/passwd', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    foreach ($passwd as $line) {
        $parts = explode(':', $line);
        echo "User: {$parts[0]}, Shell: " . end($parts) . PHP_EOL;
    }
}
?>

File wrapper operations respect standard Unix permissions and can work with special files like /dev/null or named pipes (FIFOs).

HTTP/HTTPS Wrappers

The HTTP wrappers enable web resource access with broad HTTP protocol support, handling redirects, authentication, custom headers, and different HTTP methods through stream contexts.

<?php
// Simple GET request
$apiResponse = file_get_contents('https://api.github.com/users/octocat');
$userData = json_decode($apiResponse, true);

// Advanced HTTP request with context
$postData = json_encode(['name' => 'test', 'value' => 42]);

$context = stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' => [
            'Content-Type: application/json',
            'Authorization: Bearer ' . $apiToken,
            'User-Agent: MyApp/1.0'
        ],
        'content' => $postData,
        'timeout' => 30,
        'ignore_errors' => true  // Do not throw on HTTP errors
    ]
]);

$response = file_get_contents('https://api.example.com/data', false, $context);

// Check response status from $http_response_header
if (isset($http_response_header[0])) {
    preg_match('/HTTP\/\d\.\d\s+(\d+)/', $http_response_header[0], $matches);
    $statusCode = (int) ($matches[1] ?? 0);

    if ($statusCode >= 200 && $statusCode < 300) {
        echo "Success: " . $response;
    } else {
        echo "HTTP Error {$statusCode}: " . $response;
    }
}
?>

The $http_response_header variable automatically contains response headers, enabling status code checks and header parsing. Setting ignore_errors stops file_get_contents() from returning false (and raising a warning) on HTTP error status codes, so the response body is still returned for inspection.

PHP I/O Streams (php://)

The php:// wrapper provides access to PHP's input/output streams and memory-based storage. These are essential for processing raw request data and creating temporary storage.

Standard I/O Streams

<?php
// Read raw POST data (useful for APIs)
$rawInput = file_get_contents('php://input');
$jsonData = json_decode($rawInput, true);

// Write to error log
$errorLog = fopen('php://stderr', 'w');
fwrite($errorLog, "Critical error occurred at " . date('c') . PHP_EOL);
fclose($errorLog);

// Read from command line (CLI only)
if (php_sapi_name() === 'cli') {
    $stdin = fopen('php://stdin', 'r');
    echo "Enter your name: ";
    $name = trim(fgets($stdin));
    echo "Hello, {$name}!" . PHP_EOL;
    fclose($stdin);
}
?>

Memory and Temporary Streams

<?php
// Create in-memory stream (faster for small data)
$memory = fopen('php://memory', 'r+');
fwrite($memory, "Temporary data");
rewind($memory);
$data = fread($memory, 1024);
fclose($memory);

// Create temporary file stream (better for large data)
$temp = fopen('php://temp/maxmemory:1048576', 'r+'); // 1MB memory limit
fwrite($temp, str_repeat('Large data chunk ', 1000));

// Stream automatically switches to filesystem when memory limit exceeded
echo "Stream metadata: ";
var_dump(stream_get_meta_data($temp));
fclose($temp);

// Using php://temp for CSV processing
function processLargeCsv(array $data): string
{
    $temp = fopen('php://temp', 'r+');

    foreach ($data as $row) {
        fputcsv($temp, $row);
    }

    rewind($temp);
    $csvContent = stream_get_contents($temp);
    fclose($temp);

    return $csvContent;
}
?>

Data URI Scheme (data://)

The data:// wrapper implements RFC 2397 for embedding data directly in URLs. data:// and data: are interchangeable; both refer to the same data URI scheme.

<?php
// Plain text data
$textData = file_get_contents('data://text/plain;charset=utf-8,Hello%20World');
echo $textData; // Outputs: Hello World

// Base64 encoded data
$base64Data = 'data://text/plain;base64,SGVsbG8gUEhQIERldmVsb3BlcnM=';
$decoded = file_get_contents($base64Data);
echo $decoded; // Outputs: Hello PHP Developers

// Binary data (image example)
$imageData = 'data://image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==';
$pngContent = file_get_contents($imageData);

// Save embedded image to file
file_put_contents('/tmp/tiny.png', $pngContent);

// JSON data embedded in data URI
$jsonUri = 'data://application/json;charset=utf-8,' . urlencode(json_encode([
    'users' => ['alice', 'bob', 'charlie'],
    'timestamp' => time()
]));

$jsonData = json_decode(file_get_contents($jsonUri), true);
print_r($jsonData);

// Creating dynamic data URIs
function createDataUri(string $content, string $mimeType = 'text/plain', bool $base64 = false): string
{
    if ($base64) {
        return "data://{$mimeType};base64," . base64_encode($content);
    }

    return "data://{$mimeType}," . urlencode($content);
}

$csvData = "Name,Age\nJohn,30\nJane,25";
$csvUri = createDataUri($csvData, 'text/csv');
$rows = file($csvUri, FILE_IGNORE_NEW_LINES);
?>

Data URIs are particularly useful for testing, embedding small resources, and creating self-contained applications that don't depend on external files.

Compression Wrappers

PHP provides compression wrappers for transparent handling of compressed data. The most common are zlib:// and compress.zlib:// for gzip compression.

<?php
// Reading compressed files
$compressedLog = file_get_contents('compress.zlib:///var/log/app.log.gz');
$logLines = explode("\n", $compressedLog);

// Writing compressed data
$data = str_repeat("Log entry " . date('c') . "\n", 1000);
file_put_contents('compress.zlib:///tmp/output.gz', $data);

// Compressing streaming data: compress.zlib:// expects a real filesystem
// path (it works like gzopen()), so wrapping another stream URL like
// php://output doesn't work. Use a stream filter on the destination stream
// instead.
$input = fopen('php://input', 'r');
$output = fopen('php://output', 'w');
stream_filter_append($output, 'zlib.deflate', STREAM_FILTER_WRITE);

while (!feof($input)) {
    $chunk = fread($input, 8192);
    fwrite($output, $chunk);
}

fclose($input);
fclose($output);

// Using compression filters directly
$originalSize = strlen($data);
$compressed = gzencode($data);
$compressedSize = strlen($compressed);

echo "Compression ratio: " . round(($originalSize - $compressedSize) / $originalSize * 100, 2) . "%\n";

// Decompressing with error handling
function safeDecompress(string $filePath): ?string
{
    if (!file_exists($filePath)) {
        return null;
    }

    $handle = fopen("compress.zlib://{$filePath}", 'r');
    if ($handle === false) {
        return null;
    }

    $content = stream_get_contents($handle);
    fclose($handle);

    return $content !== false ? $content : null;
}
?>

Implementing Custom Stream Wrappers

Custom stream wrappers enable access to specialised data sources through PHP's standard file functions. Use stream_wrapper_register() to register custom protocols.

Basic Stream Wrapper Class

<?php
class MemoryCache
{
    private static array $cache = [];

    public static function set(string $key, mixed $value): void
    {
        self::$cache[$key] = serialize($value);
    }

    public static function get(string $key): mixed
    {
        return isset(self::$cache[$key]) ? unserialize(self::$cache[$key]) : null;
    }

    public static function exists(string $key): bool
    {
        return isset(self::$cache[$key]);
    }

    public static function delete(string $key): void
    {
        unset(self::$cache[$key]);
    }
}

class CacheStreamWrapper
{
    private mixed $position = 0;
    private string $data = '';
    private string $key = '';
    private string $mode = '';

    public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool
    {
        $url = parse_url($path);
        $this->key = ltrim($url['path'], '/');
        $this->mode = $mode;
        $this->position = 0;

        if (str_contains($mode, 'r')) {
            // Reading mode
            $this->data = MemoryCache::get($this->key) ?? '';
            return true;
        } elseif (str_contains($mode, 'w') || str_contains($mode, 'a')) {
            // Writing mode
            $this->data = str_contains($mode, 'a') ? (MemoryCache::get($this->key) ?? '') : '';
            return true;
        }

        return false;
    }

    public function stream_read(int $count): string
    {
        $ret = substr($this->data, $this->position, $count);
        $this->position += strlen($ret);
        return $ret;
    }

    public function stream_write(string $data): int
    {
        $left = substr($this->data, 0, $this->position);
        $right = substr($this->data, $this->position + strlen($data));
        $this->data = $left . $data . $right;
        $this->position += strlen($data);
        return strlen($data);
    }

    public function stream_tell(): int
    {
        return $this->position;
    }

    public function stream_eof(): bool
    {
        return $this->position >= strlen($this->data);
    }

    public function stream_seek(int $offset, int $whence = SEEK_SET): bool
    {
        switch ($whence) {
            case SEEK_SET:
                $this->position = $offset;
                break;
            case SEEK_CUR:
                $this->position += $offset;
                break;
            case SEEK_END:
                $this->position = strlen($this->data) + $offset;
                break;
            default:
                return false;
        }
        return true;
    }

    public function stream_close(): void
    {
        if (str_contains($this->mode, 'w') || str_contains($this->mode, 'a')) {
            MemoryCache::set($this->key, $this->data);
        }
    }

    public function stream_stat(): array
    {
        return [
            'dev' => 0,
            'ino' => 0,
            'mode' => 0100644, // Regular file, readable/writable
            'nlink' => 1,
            'uid' => 0,
            'gid' => 0,
            'rdev' => 0,
            'size' => strlen($this->data),
            'atime' => time(),
            'mtime' => time(),
            'ctime' => time(),
            'blksize' => -1,
            'blocks' => -1,
        ];
    }

    public function url_stat(string $path, int $flags): array
    {
        $url = parse_url($path);
        $key = ltrim($url['path'], '/');

        if (!MemoryCache::exists($key)) {
            return [];
        }

        $data = MemoryCache::get($key) ?? '';
        return [
            'dev' => 0,
            'ino' => 0,
            'mode' => 0100644,
            'nlink' => 1,
            'uid' => 0,
            'gid' => 0,
            'rdev' => 0,
            'size' => strlen($data),
            'atime' => time(),
            'mtime' => time(),
            'ctime' => time(),
            'blksize' => -1,
            'blocks' => -1,
        ];
    }
}

// Register the custom wrapper
stream_wrapper_register('cache', CacheStreamWrapper::class);
?>

Using the Custom Stream Wrapper

<?php
// Write data to cache
$handle = fopen('cache://user:123', 'w');
fwrite($handle, json_encode(['name' => 'John', 'email' => '[email protected]']));
fclose($handle);

// Read data from cache
$userData = file_get_contents('cache://user:123');
$user = json_decode($userData, true);
echo "User: {$user['name']} ({$user['email']})\n";

// Check if cache entry exists
if (file_exists('cache://user:123')) {
    echo "Cache entry found\n";
    $stats = stat('cache://user:123');
    echo "Size: {$stats['size']} bytes\n";
}

// Append to cache entry
$handle = fopen('cache://user:123', 'a');
fwrite($handle, " - Updated at " . date('c'));
fclose($handle);

// Advanced usage with JSON files
function saveCachedJson(string $key, array $data): void
{
    file_put_contents("cache://{$key}", json_encode($data, JSON_PRETTY_PRINT));
}

function loadCachedJson(string $key): ?array
{
    if (!file_exists("cache://{$key}")) {
        return null;
    }

    $content = file_get_contents("cache://{$key}");
    return json_decode($content, true);
}

// Usage
saveCachedJson('config', ['debug' => true, 'timeout' => 30]);
$config = loadCachedJson('config');
var_dump($config);
?>

Advanced Stream Wrapper Features

Stream wrappers can implement additional methods for directory operations, metadata handling, and advanced file operations like locking and truncation.

<?php
class LogStreamWrapper
{
    private $handle;
    private string $logFile;

    public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool
    {
        $url = parse_url($path);
        $logLevel = $url['host'] ?? 'info';
        $this->logFile = "/var/log/app-{$logLevel}.log";

        // Add timestamp prefix to all writes
        if (str_contains($mode, 'w') || str_contains($mode, 'a')) {
            $this->handle = fopen($this->logFile, $mode);
            return $this->handle !== false;
        }

        return false;
    }

    public function stream_write(string $data): int
    {
        $timestamp = date('[Y-m-d H:i:s] ');
        $logEntry = $timestamp . $data;

        // Ensure newline ending
        if (!str_ends_with($logEntry, "\n")) {
            $logEntry .= "\n";
        }

        return fwrite($this->handle, $logEntry);
    }

    public function stream_close(): void
    {
        if ($this->handle) {
            fclose($this->handle);
        }
    }

    // Implement other required methods...
    public function stream_eof(): bool { return feof($this->handle); }
    public function stream_read(int $count): string { return fread($this->handle, $count); }
    public function stream_seek(int $offset, int $whence = SEEK_SET): bool { return fseek($this->handle, $offset, $whence) === 0; }
    public function stream_tell(): int { return ftell($this->handle); }

    public function stream_lock(int $operation): bool
    {
        return flock($this->handle, $operation);
    }

    public function stream_truncate(int $new_size): bool
    {
        return ftruncate($this->handle, $new_size);
    }
}

stream_wrapper_register('log', LogStreamWrapper::class);

// Usage
$errorLog = fopen('log://error/application', 'a');
fwrite($errorLog, 'Database connection failed');
flock($errorLog, LOCK_EX);
fwrite($errorLog, 'Critical error in payment processing');
flock($errorLog, LOCK_UN);
fclose($errorLog);

// Reading logs with automatic timestamping
$debugHandle = fopen('log://debug/application', 'w');
fwrite($debugHandle, 'User login attempt');
fwrite($debugHandle, 'Session created successfully');
fclose($debugHandle);
?>

Stream Filters and Contexts

Stream filters provide data transformation during read/write operations, whilst stream contexts configure wrapper behaviour.

<?php
// Using filters with streams
$data = "The quick brown fox jumps over the lazy dog.";

// ROT13 encoding
$encoded = fopen('php://memory', 'r+');
stream_filter_append($encoded, 'string.rot13');
fwrite($encoded, $data);
rewind($encoded);
$rot13Data = stream_get_contents($encoded);
fclose($encoded);

echo "Original: {$data}\n";
echo "ROT13: {$rot13Data}\n";

// Base64 encoding filter
$base64Stream = fopen('php://memory', 'r+');
stream_filter_append($base64Stream, 'convert.base64-encode');
fwrite($base64Stream, $data);
rewind($base64Stream);
$base64Data = stream_get_contents($base64Stream);
fclose($base64Stream);

echo "Base64: {$base64Data}\n";

// HTTP context with custom options
$httpContext = stream_context_create([
    'http' => [
        'method' => 'GET',
        'header' => 'Accept: application/json',
        'timeout' => 10,
        'follow_location' => true,
        'max_redirects' => 3,
        'protocol_version' => 1.1,
    ]
]);

$response = file_get_contents('https://httpbin.org/json', false, $httpContext);

// SSL context for secure connections
$sslContext = stream_context_create([
    'ssl' => [
        'verify_peer' => true,
        'verify_peer_name' => true,
        'cafile' => '/etc/ssl/certs/ca-certificates.crt',
        'ciphers' => 'HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA'
    ]
]);

$secureData = file_get_contents('https://secure-api.example.com/data', false, $sslContext);
?>

Performance Considerations

Stream wrappers introduce abstraction overhead, so understanding their performance characteristics helps in choosing the right implementation for a given use case.

<?php
// Benchmarking different stream approaches
function benchmarkStreams(string $data, int $iterations = 1000): array
{
    $results = [];

    // File wrapper
    $start = microtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        file_put_contents('/tmp/benchmark.txt', $data);
        $read = file_get_contents('/tmp/benchmark.txt');
    }
    $results['file'] = microtime(true) - $start;

    // Memory stream
    $start = microtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        $memory = fopen('php://memory', 'r+');
        fwrite($memory, $data);
        rewind($memory);
        $read = stream_get_contents($memory);
        fclose($memory);
    }
    $results['memory'] = microtime(true) - $start;

    // Temp stream
    $start = microtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        $temp = fopen('php://temp', 'r+');
        fwrite($temp, $data);
        rewind($temp);
        $read = stream_get_contents($temp);
        fclose($temp);
    }
    $results['temp'] = microtime(true) - $start;

    return $results;
}

$testData = str_repeat('Performance test data ', 100);
$benchmarks = benchmarkStreams($testData);

foreach ($benchmarks as $method => $time) {
    echo "{$method}: " . round($time * 1000, 2) . "ms\n";
}

// Memory usage monitoring
function monitorStreamMemory(callable $streamOperation): array
{
    $memoryBefore = memory_get_usage(true);
    $peakBefore = memory_get_peak_usage(true);

    $streamOperation();

    $memoryAfter = memory_get_usage(true);
    $peakAfter = memory_get_peak_usage(true);

    return [
        'memory_delta' => $memoryAfter - $memoryBefore,
        'peak_delta' => $peakAfter - $peakBefore,
    ];
}

// Monitor custom wrapper memory usage
$memoryStats = monitorStreamMemory(function() {
    $cache = fopen('cache://large-data', 'w');
    for ($i = 0; $i < 10000; $i++) {
        fwrite($cache, "Data chunk {$i}\n");
    }
    fclose($cache);
});

echo "Memory used: " . number_format($memoryStats['memory_delta']) . " bytes\n";
echo "Peak memory: " . number_format($memoryStats['peak_delta']) . " bytes\n";
?>

Security Considerations

Stream wrappers can introduce security vulnerabilities if not properly validated, so always sanitise input and implement appropriate access controls.

<?php
class SecureFileWrapper
{
    private array $allowedPaths = [
        '/var/www/uploads/',
        '/tmp/app/',
    ];

    private $handle;
    private string $realPath;

    public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool
    {
        $url = parse_url($path);
        $requestedPath = $url['path'] ?? '';

        // Resolve real path to prevent directory traversal
        $this->realPath = realpath(dirname($requestedPath)) . '/' . basename($requestedPath);

        // Check if path is within allowed directories
        foreach ($this->allowedPaths as $allowedPath) {
            if (str_starts_with($this->realPath, realpath($allowedPath))) {
                $this->handle = fopen($this->realPath, $mode);
                return $this->handle !== false;
            }
        }

        // Log security violation
        error_log("Security violation: Attempted access to {$requestedPath}");
        return false;
    }

    public function stream_read(int $count): string
    {
        return fread($this->handle, $count);
    }

    public function stream_write(string $data): int
    {
        // Filter dangerous content
        $data = $this->sanitizeContent($data);
        return fwrite($this->handle, $data);
    }

    private function sanitizeContent(string $content): string
    {
        // Remove null bytes
        $content = str_replace("\0", '', $content);

        // Basic XSS prevention for text files
        if (str_ends_with($this->realPath, '.txt') || str_ends_with($this->realPath, '.log')) {
            $content = htmlspecialchars($content, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        }

        return $content;
    }

    // Implement other required methods...
    public function stream_close(): void { if ($this->handle) fclose($this->handle); }
    public function stream_eof(): bool { return feof($this->handle); }
    public function stream_seek(int $offset, int $whence = SEEK_SET): bool { return fseek($this->handle, $offset, $whence) === 0; }
    public function stream_tell(): int { return ftell($this->handle); }
}

stream_wrapper_register('secure', SecureFileWrapper::class);

// Safe usage. stream_open() returning false makes fopen() return false
// and emit an E_WARNING; it never throws, so check the return value
// directly rather than wrapping this in try/catch.
$handle = fopen('secure:///var/www/uploads/user-data.txt', 'w');
if ($handle) {
    fwrite($handle, "Safe content");
    fclose($handle);
} else {
    echo "Access denied";
}

// This will fail due to path traversal attempt
$maliciousHandle = fopen('secure:///var/www/uploads/../../../etc/passwd', 'r');
// Returns false and logs security violation
?>

Real-World Applications

Stream wrappers excel in scenarios requiring abstraction over data sources, protocol translation, or transparent data transformation. Here are practical implementations:

Configuration Management

<?php
class ConfigStreamWrapper
{
    private static array $config = [];
    private string $data = '';
    private int $position = 0;

    public static function setConfig(array $config): void
    {
        self::$config = $config;
    }

    public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool
    {
        $url = parse_url($path);
        $configPath = ltrim($url['path'], '/');

        // Navigate nested config using dot notation
        $value = self::$config;
        foreach (explode('.', $configPath) as $key) {
            if (!isset($value[$key])) {
                return false;
            }
            $value = $value[$key];
        }

        $this->data = is_string($value) ? $value : json_encode($value, JSON_PRETTY_PRINT);
        $this->position = 0;

        return true;
    }

    public function stream_read(int $count): string
    {
        $ret = substr($this->data, $this->position, $count);
        $this->position += strlen($ret);
        return $ret;
    }

    public function stream_eof(): bool
    {
        return $this->position >= strlen($this->data);
    }

    public function stream_tell(): int { return $this->position; }
    public function stream_seek(int $offset, int $whence = SEEK_SET): bool { /* Implementation */ return true; }
}

stream_wrapper_register('config', ConfigStreamWrapper::class);

// Setup configuration
ConfigStreamWrapper::setConfig([
    'database' => [
        'host' => 'localhost',
        'port' => 3306,
        'credentials' => [
            'username' => 'app_user',
            'password' => 'secure_password'
        ]
    ],
    'cache' => [
        'driver' => 'redis',
        'ttl' => 3600
    ]
]);

// Access nested configuration values
$dbHost = file_get_contents('config://database.host');
$credentials = json_decode(file_get_contents('config://database.credentials'), true);
$cacheConfig = json_decode(file_get_contents('config://cache'), true);

echo "Database: {$dbHost}:{$credentials['username']}\n";
echo "Cache TTL: {$cacheConfig['ttl']} seconds\n";
?>

Debugging and Troubleshooting

Effective debugging of stream operations requires understanding metadata, error handling, and logging techniques.

<?php
// Comprehensive stream debugging
function debugStream(string $streamUrl): array
{
    $info = [];

    // Basic stream information
    $info['wrappers'] = stream_get_wrappers();
    $info['url_components'] = parse_url($streamUrl);

    // Test stream accessibility
    $context = stream_context_create();
    $handle = @fopen($streamUrl, 'r', false, $context);

    if ($handle === false) {
        $info['status'] = 'failed';
        $info['error'] = error_get_last();
        return $info;
    }

    // Stream metadata
    $info['status'] = 'success';
    $info['metadata'] = stream_get_meta_data($handle);

    // Read capabilities
    $info['can_read'] = !feof($handle);
    $info['position'] = ftell($handle);

    // Try to read first 100 bytes
    $preview = fread($handle, 100);
    $info['preview'] = bin2hex($preview);
    $info['preview_text'] = mb_convert_encoding($preview, 'UTF-8', 'auto');

    fclose($handle);
    return $info;
}

// Test different streams
$streams = [
    'file:///etc/hostname',
    'php://memory',
    'data://text/plain;base64,SGVsbG8gV29ybGQ=',
    'https://httpbin.org/json'
];

foreach ($streams as $stream) {
    echo "Testing {$stream}:\n";
    $debug = debugStream($stream);
    echo json_encode($debug, JSON_PRETTY_PRINT) . "\n\n";
}

// Error handling for custom wrappers
class DiagnosticStreamWrapper
{
    private static array $logs = [];

    public static function getLogs(): array
    {
        return self::$logs;
    }

    public static function clearLogs(): void
    {
        self::$logs = [];
    }

    private function log(string $method, array $args = []): void
    {
        self::$logs[] = [
            'timestamp' => microtime(true),
            'method' => $method,
            'args' => $args,
            'memory' => memory_get_usage()
        ];
    }

    public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool
    {
        $this->log(__FUNCTION__, ['path' => $path, 'mode' => $mode]);
        return true;
    }

    public function stream_read(int $count): string
    {
        $this->log(__FUNCTION__, ['count' => $count]);
        return str_repeat('X', min($count, 10)); // Return dummy data
    }

    public function stream_write(string $data): int
    {
        $this->log(__FUNCTION__, ['length' => strlen($data)]);
        return strlen($data);
    }

    public function stream_eof(): bool
    {
        $this->log(__FUNCTION__);
        return false;
    }

    public function stream_close(): void
    {
        $this->log(__FUNCTION__);
    }

    public function stream_tell(): int { return 0; }
    public function stream_seek(int $offset, int $whence = SEEK_SET): bool { return true; }
}

stream_wrapper_register('debug', DiagnosticStreamWrapper::class);

// Test diagnostic wrapper
$handle = fopen('debug://test', 'r+');
fread($handle, 50);
fwrite($handle, 'test data');
fclose($handle);

// Review diagnostic logs
$logs = DiagnosticStreamWrapper::getLogs();
foreach ($logs as $log) {
    echo sprintf("[%.4f] %s: %s\n",
        $log['timestamp'],
        $log['method'],
        json_encode($log['args'])
    );
}
?>

Conclusion

The key to effective stream wrapper usage lies in understanding the abstraction's strengths: protocol independence, transparent data transformation, and clean integration with existing code. The built-in wrappers cover most day-to-day needs (file://, http://, php://), whilst custom implementations are worth reaching for when a project needs specialised handling such as caching, logging, or secure file access.

Got a PHP system that needs this kind of attention?

Get in touch