Navigation

Php

PHP Stream Wrappers: Creating Custom Protocol Handlers

Master PHP stream wrappers to create custom protocol handlers and virtual file systems. Learn to build powerful abstractions for databases, APIs, cloud storage, and custom data sources with seamless file-like interfaces.

Summary: Master PHP stream wrappers to create custom protocol handlers and virtual file systems. Learn to build powerful abstractions for databases, APIs, cloud storage, and custom data sources with seamless file-like interfaces.

Table Of Contents

Introduction

PHP stream wrappers provide one of the most powerful yet underutilized features in PHP, allowing you to create custom protocol handlers that integrate seamlessly with PHP's file system functions. By implementing stream wrappers, you can make databases, APIs, cloud storage, and any custom data source appear as standard file systems, enabling the use of familiar functions like file_get_contents(), fopen(), and file_put_contents() with your custom protocols.

Stream wrappers unlock incredible possibilities for abstraction and integration. Whether you're building a virtual file system over a database, creating transparent cloud storage access, or implementing custom caching layers, stream wrappers provide the foundation for elegant, maintainable solutions that feel natural to PHP developers while hiding complex underlying implementations.

Understanding Stream Wrappers

How Stream Wrappers Work

Stream wrappers register custom protocols that PHP's stream functions can handle transparently:

// Basic stream wrapper structure
class CustomStreamWrapper
{
    public $context;
    private $position;
    private $data;

    // Required methods for read operations
    public function stream_open($path, $mode, $options, &$opened_path) { /* ... */ }
    public function stream_read($count) { /* ... */ }
    public function stream_write($data) { /* ... */ }
    public function stream_tell() { /* ... */ }
    public function stream_seek($offset, $whence) { /* ... */ }
    public function stream_eof() { /* ... */ }
    public function stream_close() { /* ... */ }
    
    // Optional methods for advanced functionality
    public function stream_stat() { /* ... */ }
    public function url_stat($path, $flags) { /* ... */ }
    public function stream_metadata($path, $option, $value) { /* ... */ }
    
    // Directory operations
    public function dir_opendir($path, $options) { /* ... */ }
    public function dir_readdir() { /* ... */ }
    public function dir_rewinddir() { /* ... */ }
    public function dir_closedir() { /* ... */ }
}

// Register the wrapper
stream_wrapper_register('custom', CustomStreamWrapper::class);

// Now you can use it like any file system
$content = file_get_contents('custom://path/to/resource');
file_put_contents('custom://path/to/file', $data);

Stream Wrapper Interface Methods

Essential methods every stream wrapper should implement:

// src/StreamWrappers/BaseStreamWrapper.php
<?php

namespace App\StreamWrappers;

abstract class BaseStreamWrapper
{
    public $context;
    protected $position = 0;
    protected $data = '';
    protected $path = '';
    protected $mode = '';

    /**
     * Opens file or URL
     */
    abstract public function stream_open($path, $mode, $options, &$opened_path);

    /**
     * Read from stream
     */
    public function stream_read($count)
    {
        $result = substr($this->data, $this->position, $count);
        $this->position += strlen($result);
        return $result;
    }

    /**
     * Write to stream
     */
    public function stream_write($data)
    {
        $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);
    }

    /**
     * Retrieve current position of stream
     */
    public function stream_tell()
    {
        return $this->position;
    }

    /**
     * Tests for end-of-file
     */
    public function stream_eof()
    {
        return $this->position >= strlen($this->data);
    }

    /**
     * Seeks to specific location in stream
     */
    public function stream_seek($offset, $whence = SEEK_SET)
    {
        switch ($whence) {
            case SEEK_SET:
                if ($offset < strlen($this->data) && $offset >= 0) {
                    $this->position = $offset;
                    return true;
                }
                break;
            case SEEK_CUR:
                if ($offset >= 0) {
                    $this->position += $offset;
                    return true;
                }
                break;
            case SEEK_END:
                if (strlen($this->data) + $offset >= 0) {
                    $this->position = strlen($this->data) + $offset;
                    return true;
                }
                break;
        }
        return false;
    }

    /**
     * Close stream
     */
    public function stream_close()
    {
        return true;
    }

    /**
     * Retrieve information about a file resource
     */
    public function stream_stat()
    {
        return [
            'dev' => 0,
            'ino' => 0,
            'mode' => 33188, // Regular file permissions
            'nlink' => 1,
            'uid' => 0,
            'gid' => 0,
            'rdev' => 0,
            'size' => strlen($this->data),
            'atime' => time(),
            'mtime' => time(),
            'ctime' => time(),
            'blksize' => 4096,
            'blocks' => ceil(strlen($this->data) / 4096)
        ];
    }

    /**
     * Retrieve information about a file
     */
    abstract public function url_stat($path, $flags);

    /**
     * Parse URL to extract components
     */
    protected function parseUrl($url)
    {
        $parts = parse_url($url);
        
        if ($parts === false) {
            throw new \InvalidArgumentException("Invalid URL: {$url}");
        }

        return [
            'scheme' => $parts['scheme'] ?? '',
            'host' => $parts['host'] ?? '',
            'port' => $parts['port'] ?? null,
            'user' => $parts['user'] ?? '',
            'pass' => $parts['pass'] ?? '',
            'path' => $parts['path'] ?? '/',
            'query' => $parts['query'] ?? '',
            'fragment' => $parts['fragment'] ?? ''
        ];
    }

    /**
     * Get context options
     */
    protected function getContextOptions()
    {
        if ($this->context) {
            return stream_context_get_options($this->context);
        }
        return [];
    }
}

Advanced Stream Wrapper Implementations

Database Stream Wrapper

Create a stream wrapper that treats database records as files:

// src/StreamWrappers/DatabaseStreamWrapper.php
<?php

namespace App\StreamWrappers;

use PDO;
use PDOException;

class DatabaseStreamWrapper extends BaseStreamWrapper
{
    private $pdo;
    private $table;
    private $keyColumn;
    private $contentColumn;
    private $key;
    private $isWritable = false;

    public function stream_open($path, $mode, $options, &$opened_path)
    {
        try {
            $url = $this->parseUrl($path);
            $this->connectToDatabase($url);
            
            // Parse path: db://table/key or db://table/key?content_column=data
            $pathParts = explode('/', trim($url['path'], '/'));
            
            if (count($pathParts) < 2) {
                throw new \InvalidArgumentException('Invalid path format. Use: db://table/key');
            }

            $this->table = $pathParts[0];
            $this->key = $pathParts[1];
            
            // Parse query parameters
            parse_str($url['query'], $queryParams);
            $this->keyColumn = $queryParams['key_column'] ?? 'id';
            $this->contentColumn = $queryParams['content_column'] ?? 'content';

            $this->mode = $mode;
            $this->isWritable = strpbrk($mode, 'wax+') !== false;

            // Load existing data for read/append modes
            if (strpos($mode, 'r') !== false || strpos($mode, 'a') !== false) {
                $this->loadData();
            } else {
                $this->data = '';
            }

            if (strpos($mode, 'a') !== false) {
                $this->position = strlen($this->data);
            } else {
                $this->position = 0;
            }

            return true;

        } catch (\Exception $e) {
            trigger_error($e->getMessage(), E_USER_WARNING);
            return false;
        }
    }

    public function stream_write($data)
    {
        if (!$this->isWritable) {
            return false;
        }

        $bytesWritten = parent::stream_write($data);
        
        // Auto-save on write (can be configured)
        $this->saveData();
        
        return $bytesWritten;
    }

    public function stream_close()
    {
        if ($this->isWritable && $this->data !== null) {
            $this->saveData();
        }
        return parent::stream_close();
    }

    public function url_stat($path, $flags)
    {
        try {
            $url = $this->parseUrl($path);
            $this->connectToDatabase($url);
            
            $pathParts = explode('/', trim($url['path'], '/'));
            $table = $pathParts[0];
            $key = $pathParts[1] ?? null;

            if (!$key) {
                return false;
            }

            parse_str($url['query'], $queryParams);
            $keyColumn = $queryParams['key_column'] ?? 'id';
            $contentColumn = $queryParams['content_column'] ?? 'content';

            $stmt = $this->pdo->prepare("
                SELECT LENGTH({$contentColumn}) as size, 
                       UNIX_TIMESTAMP(created_at) as ctime,
                       UNIX_TIMESTAMP(updated_at) as mtime
                FROM {$table} 
                WHERE {$keyColumn} = ?
            ");
            
            $stmt->execute([$key]);
            $result = $stmt->fetch(PDO::FETCH_ASSOC);

            if (!$result) {
                return false;
            }

            return [
                'dev' => 0,
                'ino' => crc32($path),
                'mode' => 33188,
                'nlink' => 1,
                'uid' => 0,
                'gid' => 0,
                'rdev' => 0,
                'size' => (int)$result['size'],
                'atime' => time(),
                'mtime' => (int)($result['mtime'] ?: time()),
                'ctime' => (int)($result['ctime'] ?: time()),
                'blksize' => 4096,
                'blocks' => ceil($result['size'] / 4096)
            ];

        } catch (\Exception $e) {
            return false;
        }
    }

    public function stream_metadata($path, $option, $value)
    {
        switch ($option) {
            case STREAM_META_TOUCH:
                // Update timestamps
                try {
                    $url = $this->parseUrl($path);
                    $this->connectToDatabase($url);
                    
                    $pathParts = explode('/', trim($url['path'], '/'));
                    $table = $pathParts[0];
                    $key = $pathParts[1];

                    parse_str($url['query'], $queryParams);
                    $keyColumn = $queryParams['key_column'] ?? 'id';

                    $stmt = $this->pdo->prepare("
                        UPDATE {$table} 
                        SET updated_at = CURRENT_TIMESTAMP 
                        WHERE {$keyColumn} = ?
                    ");
                    
                    return $stmt->execute([$key]);
                } catch (\Exception $e) {
                    return false;
                }

            case STREAM_META_ACCESS:
                // This would require a permissions system
                return true;

            default:
                return false;
        }
    }

    public function unlink($path)
    {
        try {
            $url = $this->parseUrl($path);
            $this->connectToDatabase($url);
            
            $pathParts = explode('/', trim($url['path'], '/'));
            $table = $pathParts[0];
            $key = $pathParts[1];

            parse_str($url['query'], $queryParams);
            $keyColumn = $queryParams['key_column'] ?? 'id';

            $stmt = $this->pdo->prepare("DELETE FROM {$table} WHERE {$keyColumn} = ?");
            return $stmt->execute([$key]);

        } catch (\Exception $e) {
            return false;
        }
    }

    private function connectToDatabase($url)
    {
        if ($this->pdo) {
            return;
        }

        $contextOptions = $this->getContextOptions();
        $dbOptions = $contextOptions['db'] ?? [];

        $dsn = $dbOptions['dsn'] ?? "mysql:host={$url['host']};port=" . ($url['port'] ?? 3306);
        $username = $url['user'] ?: ($dbOptions['username'] ?? 'root');
        $password = $url['pass'] ?: ($dbOptions['password'] ?? '');
        $options = $dbOptions['options'] ?? [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
        ];

        $this->pdo = new PDO($dsn, $username, $password, $options);
    }

    private function loadData()
    {
        try {
            $stmt = $this->pdo->prepare("
                SELECT {$this->contentColumn} 
                FROM {$this->table} 
                WHERE {$this->keyColumn} = ?
            ");
            
            $stmt->execute([$this->key]);
            $result = $stmt->fetch(PDO::FETCH_ASSOC);
            
            $this->data = $result ? $result[$this->contentColumn] : '';
            
        } catch (PDOException $e) {
            $this->data = '';
        }
    }

    private function saveData()
    {
        try {
            $stmt = $this->pdo->prepare("
                INSERT INTO {$this->table} ({$this->keyColumn}, {$this->contentColumn}) 
                VALUES (?, ?) 
                ON DUPLICATE KEY UPDATE 
                {$this->contentColumn} = VALUES({$this->contentColumn}),
                updated_at = CURRENT_TIMESTAMP
            ");
            
            return $stmt->execute([$this->key, $this->data]);
            
        } catch (PDOException $e) {
            trigger_error("Failed to save data: " . $e->getMessage(), E_USER_WARNING);
            return false;
        }
    }
}

// Register the wrapper
stream_wrapper_register('db', DatabaseStreamWrapper::class);

// Usage examples:
// $content = file_get_contents('db://posts/123');
// file_put_contents('db://posts/124', 'New content');
// $exists = file_exists('db://posts/125');

Cloud Storage Stream Wrapper

Implement a stream wrapper for cloud storage services:

// src/StreamWrappers/CloudStreamWrapper.php
<?php

namespace App\StreamWrappers;

class CloudStreamWrapper extends BaseStreamWrapper
{
    private $client;
    private $bucket;
    private $objectKey;
    private $contentType;
    private $metadata = [];

    public function stream_open($path, $mode, $options, &$opened_path)
    {
        try {
            $url = $this->parseUrl($path);
            $this->initializeClient($url);
            
            $this->bucket = $url['host'];
            $this->objectKey = ltrim($url['path'], '/');
            $this->mode = $mode;

            // Load existing object for read/append modes
            if (strpos($mode, 'r') !== false || strpos($mode, 'a') !== false) {
                $this->loadObject();
            } else {
                $this->data = '';
                $this->contentType = 'application/octet-stream';
            }

            if (strpos($mode, 'a') !== false) {
                $this->position = strlen($this->data);
            } else {
                $this->position = 0;
            }

            return true;

        } catch (\Exception $e) {
            trigger_error($e->getMessage(), E_USER_WARNING);
            return false;
        }
    }

    public function stream_write($data)
    {
        $bytesWritten = parent::stream_write($data);
        
        // Auto-detect content type based on data
        if (empty($this->contentType) || $this->contentType === 'application/octet-stream') {
            $this->contentType = $this->detectContentType($data);
        }
        
        return $bytesWritten;
    }

    public function stream_close()
    {
        if (strpbrk($this->mode, 'wax+') !== false && $this->data !== null) {
            $this->saveObject();
        }
        return parent::stream_close();
    }

    public function url_stat($path, $flags)
    {
        try {
            $url = $this->parseUrl($path);
            $this->initializeClient($url);
            
            $bucket = $url['host'];
            $objectKey = ltrim($url['path'], '/');

            $result = $this->client->headObject([
                'Bucket' => $bucket,
                'Key' => $objectKey
            ]);

            $size = $result['ContentLength'] ?? 0;
            $mtime = $result['LastModified'] ? $result['LastModified']->getTimestamp() : time();

            return [
                'dev' => 0,
                'ino' => crc32($path),
                'mode' => 33188,
                'nlink' => 1,
                'uid' => 0,
                'gid' => 0,
                'rdev' => 0,
                'size' => $size,
                'atime' => time(),
                'mtime' => $mtime,
                'ctime' => $mtime,
                'blksize' => 4096,
                'blocks' => ceil($size / 4096)
            ];

        } catch (\Exception $e) {
            return false;
        }
    }

    public function unlink($path)
    {
        try {
            $url = $this->parseUrl($path);
            $this->initializeClient($url);
            
            $bucket = $url['host'];
            $objectKey = ltrim($url['path'], '/');

            $this->client->deleteObject([
                'Bucket' => $bucket,
                'Key' => $objectKey
            ]);

            return true;

        } catch (\Exception $e) {
            return false;
        }
    }

    public function stream_metadata($path, $option, $value)
    {
        switch ($option) {
            case STREAM_META_TOUCH:
                // Create empty object if it doesn't exist
                try {
                    $url = $this->parseUrl($path);
                    $this->initializeClient($url);
                    
                    $bucket = $url['host'];
                    $objectKey = ltrim($url['path'], '/');

                    // Check if object exists
                    try {
                        $this->client->headObject([
                            'Bucket' => $bucket,
                            'Key' => $objectKey
                        ]);
                        return true; // Already exists
                    } catch (\Exception $e) {
                        // Create empty object
                        $this->client->putObject([
                            'Bucket' => $bucket,
                            'Key' => $objectKey,
                            'Body' => '',
                            'ContentType' => 'application/octet-stream'
                        ]);
                        return true;
                    }
                } catch (\Exception $e) {
                    return false;
                }

            default:
                return false;
        }
    }

    // Directory operations for listing objects
    public function dir_opendir($path, $options)
    {
        try {
            $url = $this->parseUrl($path);
            $this->initializeClient($url);
            
            $this->bucket = $url['host'];
            $prefix = ltrim($url['path'], '/');
            
            if ($prefix && !str_ends_with($prefix, '/')) {
                $prefix .= '/';
            }

            $result = $this->client->listObjectsV2([
                'Bucket' => $this->bucket,
                'Prefix' => $prefix,
                'Delimiter' => '/'
            ]);

            $this->dirEntries = [];
            
            // Add directories (common prefixes)
            foreach ($result['CommonPrefixes'] ?? [] as $commonPrefix) {
                $dirName = rtrim(str_replace($prefix, '', $commonPrefix['Prefix']), '/');
                if ($dirName) {
                    $this->dirEntries[] = $dirName;
                }
            }

            // Add files (objects)
            foreach ($result['Contents'] ?? [] as $object) {
                $fileName = str_replace($prefix, '', $object['Key']);
                if ($fileName && !str_contains($fileName, '/')) {
                    $this->dirEntries[] = $fileName;
                }
            }

            $this->dirIndex = 0;
            return true;

        } catch (\Exception $e) {
            return false;
        }
    }

    public function dir_readdir()
    {
        if ($this->dirIndex < count($this->dirEntries)) {
            return $this->dirEntries[$this->dirIndex++];
        }
        return false;
    }

    public function dir_rewinddir()
    {
        $this->dirIndex = 0;
        return true;
    }

    public function dir_closedir()
    {
        $this->dirEntries = [];
        $this->dirIndex = 0;
        return true;
    }

    private function initializeClient($url)
    {
        if ($this->client) {
            return;
        }

        $contextOptions = $this->getContextOptions();
        $cloudOptions = $contextOptions['cloud'] ?? [];

        // Initialize cloud storage client (AWS S3, Google Cloud Storage, etc.)
        $this->client = new \Aws\S3\S3Client([
            'version' => 'latest',
            'region' => $cloudOptions['region'] ?? 'us-east-1',
            'credentials' => [
                'key' => $cloudOptions['access_key'] ?? $url['user'],
                'secret' => $cloudOptions['secret_key'] ?? $url['pass']
            ]
        ]);
    }

    private function loadObject()
    {
        try {
            $result = $this->client->getObject([
                'Bucket' => $this->bucket,
                'Key' => $this->objectKey
            ]);

            $this->data = $result['Body']->getContents();
            $this->contentType = $result['ContentType'] ?? 'application/octet-stream';
            $this->metadata = $result['Metadata'] ?? [];

        } catch (\Exception $e) {
            $this->data = '';
            $this->contentType = 'application/octet-stream';
        }
    }

    private function saveObject()
    {
        try {
            $params = [
                'Bucket' => $this->bucket,
                'Key' => $this->objectKey,
                'Body' => $this->data,
                'ContentType' => $this->contentType
            ];

            if (!empty($this->metadata)) {
                $params['Metadata'] = $this->metadata;
            }

            $this->client->putObject($params);
            return true;

        } catch (\Exception $e) {
            trigger_error("Failed to save object: " . $e->getMessage(), E_USER_WARNING);
            return false;
        }
    }

    private function detectContentType($data)
    {
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mimeType = finfo_buffer($finfo, $data);
        finfo_close($finfo);
        
        return $mimeType ?: 'application/octet-stream';
    }
}

// Register the wrapper
stream_wrapper_register('cloud', CloudStreamWrapper::class);

// Usage examples:
// $content = file_get_contents('cloud://my-bucket/path/to/file.txt');
// file_put_contents('cloud://my-bucket/uploads/new-file.pdf', $pdfData);
// $files = scandir('cloud://my-bucket/images/');

Caching Stream Wrapper

Create a transparent caching layer using stream wrappers:

// src/StreamWrappers/CacheStreamWrapper.php
<?php

namespace App\StreamWrappers;

class CacheStreamWrapper extends BaseStreamWrapper
{
    private $cacheDir;
    private $originalUrl;
    private $cacheFile;
    private $ttl;
    private $originalWrapper;

    public function stream_open($path, $mode, $options, &$opened_path)
    {
        try {
            $url = $this->parseUrl($path);
            
            // Extract original URL from cache://original_scheme://host/path format
            $originalScheme = $url['host'];
            $this->originalUrl = $originalScheme . ':/' . $url['path'];
            
            // Parse query parameters
            parse_str($url['query'], $queryParams);
            $this->ttl = (int)($queryParams['ttl'] ?? 3600); // Default 1 hour
            
            $contextOptions = $this->getContextOptions();
            $cacheOptions = $contextOptions['cache'] ?? [];
            $this->cacheDir = $cacheOptions['cache_dir'] ?? sys_get_temp_dir() . '/stream_cache';
            
            // Ensure cache directory exists
            if (!is_dir($this->cacheDir)) {
                mkdir($this->cacheDir, 0755, true);
            }

            // Generate cache file name
            $this->cacheFile = $this->cacheDir . '/' . md5($this->originalUrl) . '.cache';
            
            $this->mode = $mode;

            // For read operations, try cache first
            if (strpos($mode, 'r') !== false) {
                if ($this->loadFromCache()) {
                    return true;
                }
                
                // Cache miss - load from original source
                if ($this->loadFromOriginal()) {
                    $this->saveToCache();
                    return true;
                }
                
                return false;
            }

            // For write operations, write to cache and original
            if (strpbrk($mode, 'wax+') !== false) {
                $this->data = '';
                return true;
            }

            return false;

        } catch (\Exception $e) {
            trigger_error($e->getMessage(), E_USER_WARNING);
            return false;
        }
    }

    public function stream_write($data)
    {
        $bytesWritten = parent::stream_write($data);
        
        // For write operations, we'll write to both cache and original on close
        return $bytesWritten;
    }

    public function stream_close()
    {
        if (strpbrk($this->mode, 'wax+') !== false && $this->data !== null) {
            // Write to original source
            $this->writeToOriginal();
            
            // Update cache
            $this->saveToCache();
        }
        
        return parent::stream_close();
    }

    public function url_stat($path, $flags)
    {
        $url = $this->parseUrl($path);
        $originalScheme = $url['host'];
        $originalUrl = $originalScheme . ':/' . $url['path'];
        
        // Check cache first
        $cacheFile = $this->cacheDir . '/' . md5($originalUrl) . '.cache';
        
        if (file_exists($cacheFile) && $this->isCacheValid($cacheFile)) {
            return stat($cacheFile);
        }
        
        // Fallback to original source
        return @stat($originalUrl);
    }

    public function unlink($path)
    {
        $url = $this->parseUrl($path);
        $originalScheme = $url['host'];
        $originalUrl = $originalScheme . ':/' . $url['path'];
        
        // Clear cache
        $cacheFile = $this->cacheDir . '/' . md5($originalUrl) . '.cache';
        if (file_exists($cacheFile)) {
            unlink($cacheFile);
        }
        
        // Delete from original source
        return @unlink($originalUrl);
    }

    public function stream_metadata($path, $option, $value)
    {
        $url = $this->parseUrl($path);
        $originalScheme = $url['host'];
        $originalUrl = $originalScheme . ':/' . $url['path'];
        
        switch ($option) {
            case STREAM_META_TOUCH:
                // Invalidate cache and touch original
                $cacheFile = $this->cacheDir . '/' . md5($originalUrl) . '.cache';
                if (file_exists($cacheFile)) {
                    unlink($cacheFile);
                }
                return @touch($originalUrl);
                
            default:
                return false;
        }
    }

    private function loadFromCache()
    {
        if (!file_exists($this->cacheFile)) {
            return false;
        }
        
        if (!$this->isCacheValid($this->cacheFile)) {
            unlink($this->cacheFile);
            return false;
        }
        
        $this->data = file_get_contents($this->cacheFile);
        return true;
    }

    private function loadFromOriginal()
    {
        try {
            $context = $this->context ? stream_context_create(stream_context_get_options($this->context)) : null;
            $this->data = file_get_contents($this->originalUrl, false, $context);
            return $this->data !== false;
        } catch (\Exception $e) {
            return false;
        }
    }

    private function saveToCache()
    {
        $cacheData = [
            'timestamp' => time(),
            'ttl' => $this->ttl,
            'data' => $this->data
        ];
        
        file_put_contents($this->cacheFile, serialize($cacheData), LOCK_EX);
    }

    private function writeToOriginal()
    {
        try {
            $context = $this->context ? stream_context_create(stream_context_get_options($this->context)) : null;
            return file_put_contents($this->originalUrl, $this->data, 0, $context) !== false;
        } catch (\Exception $e) {
            return false;
        }
    }

    private function isCacheValid($cacheFile)
    {
        $cacheData = unserialize(file_get_contents($cacheFile));
        
        if (!is_array($cacheData) || !isset($cacheData['timestamp'], $cacheData['ttl'])) {
            return false;
        }
        
        return (time() - $cacheData['timestamp']) < $cacheData['ttl'];
    }
}

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

// Usage examples:
// Cache HTTP requests for 1 hour
// $content = file_get_contents('cache://http/example.com/api/data?ttl=3600');

// Cache file system access for 30 minutes
// $data = file_get_contents('cache://file/path/to/expensive/computation.json?ttl=1800');

Advanced Stream Wrapper Features

Stream Context Integration

Leverage stream contexts for configuration and authentication:

// Creating contexts for different stream wrappers
$dbContext = stream_context_create([
    'db' => [
        'dsn' => 'mysql:host=localhost;dbname=myapp',
        'username' => 'user',
        'password' => 'pass',
        'options' => [
            PDO::ATTR_TIMEOUT => 30,
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
        ]
    ]
]);

$cloudContext = stream_context_create([
    'cloud' => [
        'access_key' => 'your-access-key',
        'secret_key' => 'your-secret-key',
        'region' => 'us-west-2'
    ]
]);

$cacheContext = stream_context_create([
    'cache' => [
        'cache_dir' => '/var/cache/streams',
        'default_ttl' => 7200
    ]
]);

// Use contexts with stream operations
$content = file_get_contents('db://posts/123', false, $dbContext);
file_put_contents('cloud://bucket/file.txt', $data, 0, $cloudContext);
$cachedData = file_get_contents('cache://http/api.example.com/data', false, $cacheContext);

Stream Wrapper Factory

Create a factory for managing stream wrapper registration:

// src/StreamWrappers/StreamWrapperFactory.php
<?php

namespace App\StreamWrappers;

class StreamWrapperFactory
{
    private static $registered = [];
    private static $configurations = [];

    public static function register(string $protocol, string $wrapperClass, array $config = [])
    {
        if (isset(self::$registered[$protocol])) {
            stream_wrapper_unregister($protocol);
        }

        if (!stream_wrapper_register($protocol, $wrapperClass)) {
            throw new \RuntimeException("Failed to register stream wrapper for protocol: {$protocol}");
        }

        self::$registered[$protocol] = $wrapperClass;
        self::$configurations[$protocol] = $config;
    }

    public static function unregister(string $protocol)
    {
        if (isset(self::$registered[$protocol])) {
            stream_wrapper_unregister($protocol);
            unset(self::$registered[$protocol], self::$configurations[$protocol]);
        }
    }

    public static function createContext(string $protocol, array $options = [])
    {
        $config = self::$configurations[$protocol] ?? [];
        $mergedOptions = array_merge_recursive($config, $options);
        
        return stream_context_create([$protocol => $mergedOptions]);
    }

    public static function getRegistered(): array
    {
        return self::$registered;
    }

    public static function isRegistered(string $protocol): bool
    {
        return isset(self::$registered[$protocol]);
    }

    public static function registerBuiltIn()
    {
        // Register common stream wrappers with default configurations
        self::register('db', DatabaseStreamWrapper::class, [
            'timeout' => 30,
            'key_column' => 'id',
            'content_column' => 'content'
        ]);

        self::register('cloud', CloudStreamWrapper::class, [
            'region' => 'us-east-1',
            'timeout' => 60
        ]);

        self::register('cache', CacheStreamWrapper::class, [
            'cache_dir' => sys_get_temp_dir() . '/stream_cache',
            'default_ttl' => 3600
        ]);
    }
}

// Usage
StreamWrapperFactory::registerBuiltIn();

// Create configured contexts
$dbContext = StreamWrapperFactory::createContext('db', [
    'dsn' => 'mysql:host=localhost;dbname=myapp',
    'username' => 'user',
    'password' => 'pass'
]);

$content = file_get_contents('db://posts/123', false, $dbContext);

Performance Monitoring

Add performance monitoring to stream wrappers:

// src/StreamWrappers/Traits/MonitorableTrait.php
<?php

namespace App\StreamWrappers\Traits;

trait MonitorableTrait
{
    private $metrics = [];
    private $startTime;

    private function startTiming(string $operation)
    {
        $this->startTime = microtime(true);
        $this->metrics[$operation] = [
            'start_time' => $this->startTime,
            'start_memory' => memory_get_usage()
        ];
    }

    private function endTiming(string $operation)
    {
        if (!isset($this->metrics[$operation])) {
            return;
        }

        $endTime = microtime(true);
        $endMemory = memory_get_usage();

        $this->metrics[$operation]['end_time'] = $endTime;
        $this->metrics[$operation]['end_memory'] = $endMemory;
        $this->metrics[$operation]['duration'] = $endTime - $this->metrics[$operation]['start_time'];
        $this->metrics[$operation]['memory_used'] = $endMemory - $this->metrics[$operation]['start_memory'];

        $this->logMetrics($operation, $this->metrics[$operation]);
    }

    private function logMetrics(string $operation, array $metrics)
    {
        $logEntry = sprintf(
            "[%s] %s: %.4fs, Memory: %d bytes",
            get_class($this),
            $operation,
            $metrics['duration'],
            $metrics['memory_used']
        );

        error_log($logEntry);
    }

    public function getMetrics(): array
    {
        return $this->metrics;
    }
}

// Use in stream wrappers
class MonitoredDatabaseStreamWrapper extends DatabaseStreamWrapper
{
    use MonitorableTrait;

    public function stream_open($path, $mode, $options, &$opened_path)
    {
        $this->startTiming('stream_open');
        $result = parent::stream_open($path, $mode, $options, $opened_path);
        $this->endTiming('stream_open');
        return $result;
    }

    public function stream_read($count)
    {
        $this->startTiming('stream_read');
        $result = parent::stream_read($count);
        $this->endTiming('stream_read');
        return $result;
    }

    // ... implement for other methods
}

Real-World Applications

Configuration Stream Wrapper

Create a stream wrapper for configuration management:

// src/StreamWrappers/ConfigStreamWrapper.php
<?php

namespace App\StreamWrappers;

class ConfigStreamWrapper extends BaseStreamWrapper
{
    private $configStore;
    private $environment;
    private $key;

    public function stream_open($path, $mode, $options, &$opened_path)
    {
        $url = $this->parseUrl($path);
        
        // config://environment/key.subkey
        $pathParts = explode('/', trim($url['path'], '/'));
        $this->environment = $pathParts[0] ?? 'default';
        $this->key = $pathParts[1] ?? '';

        $contextOptions = $this->getContextOptions();
        $configOptions = $contextOptions['config'] ?? [];
        
        $this->configStore = new ConfigStore(
            $configOptions['backend'] ?? 'file',
            $configOptions['connection'] ?? []
        );

        $this->mode = $mode;

        if (strpos($mode, 'r') !== false) {
            $this->data = $this->loadConfig();
        } else {
            $this->data = '';
        }

        return true;
    }

    public function stream_close()
    {
        if (strpbrk($this->mode, 'wax+') !== false && $this->data !== null) {
            $this->saveConfig();
        }
        return parent::stream_close();
    }

    private function loadConfig()
    {
        $config = $this->configStore->get($this->environment, $this->key);
        return json_encode($config, JSON_PRETTY_PRINT);
    }

    private function saveConfig()
    {
        $config = json_decode($this->data, true);
        if (json_last_error() === JSON_ERROR_NONE) {
            $this->configStore->set($this->environment, $this->key, $config);
        }
    }
}

// Usage:
// $config = json_decode(file_get_contents('config://production/database'), true);
// file_put_contents('config://staging/app.debug', json_encode(['debug' => true]));

Related Posts

For more insights into PHP advanced features and optimization techniques, explore these related articles:

Conclusion

PHP stream wrappers represent a powerful abstraction mechanism that enables elegant integration of diverse data sources into PHP's native file system functions. By implementing custom stream wrappers, you can create seamless interfaces for databases, cloud storage, APIs, and any custom data source while maintaining familiar PHP syntax and functionality.

Key advantages of stream wrappers:

  • Transparent Integration: Make any data source appear as a file system
  • Familiar Interface: Use standard PHP file functions with custom protocols
  • Powerful Abstraction: Hide complex implementations behind simple interfaces
  • Flexible Architecture: Support for contexts, metadata, and directory operations
  • Performance Benefits: Enable caching, batching, and optimization strategies

Stream wrappers excel at creating clean abstractions that feel natural to PHP developers while providing powerful capabilities for data access and manipulation. Whether building virtual file systems, transparent caching layers, or API integrations, stream wrappers offer a robust foundation for innovative solutions.

Remember that stream wrappers require careful implementation to handle all edge cases and provide reliable behavior. Always implement proper error handling, consider performance implications, and thoroughly test your implementations across different usage patterns.

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Php