1<?php 2 3declare(strict_types=1); 4 5namespace GuzzleHttp\Psr7; 6 7use Psr\Http\Message\StreamInterface; 8 9/** 10 * Compose stream implementations based on a hash of functions. 11 * 12 * Allows for easy testing and extension of a provided stream without needing 13 * to create a concrete class for a simple extension point. 14 */ 15#[\AllowDynamicProperties] 16final class FnStream implements StreamInterface 17{ 18 private const SLOTS = [ 19 '__toString', 'close', 'detach', 'rewind', 20 'getSize', 'tell', 'eof', 'isSeekable', 'seek', 'isWritable', 'write', 21 'isReadable', 'read', 'getContents', 'getMetadata', 22 ]; 23 24 /** @var array<string, callable> */ 25 private $methods; 26 27 /** 28 * @param array<string, callable> $methods Hash of method name to a callable. 29 */ 30 public function __construct(array $methods) 31 { 32 $this->methods = $methods; 33 34 // Create the functions on the class 35 foreach ($methods as $name => $fn) { 36 $this->{'_fn_'.$name} = $fn; 37 } 38 } 39 40 /** 41 * Lazily determine which methods are not implemented. 42 * 43 * @throws \BadMethodCallException 44 */ 45 public function __get(string $name): void 46 { 47 throw new \BadMethodCallException(str_replace('_fn_', '', $name) 48 .'() is not implemented in the FnStream'); 49 } 50 51 /** 52 * The close method is called on the underlying stream only if possible. 53 */ 54 public function __destruct() 55 { 56 if (isset($this->_fn_close)) { 57 ($this->_fn_close)(); 58 } 59 } 60 61 /** 62 * An unserialize would allow the __destruct to run when the unserialized value goes out of scope. 63 * 64 * @throws \LogicException 65 */ 66 public function __wakeup(): void 67 { 68 throw new \LogicException('FnStream should never be unserialized'); 69 } 70 71 /** 72 * Adds custom functionality to an underlying stream by intercepting 73 * specific method calls. 74 * 75 * @param StreamInterface $stream Stream to decorate 76 * @param array<string, callable> $methods Hash of method name to a closure 77 * 78 * @return FnStream 79 */ 80 public static function decorate(StreamInterface $stream, array $methods) 81 { 82 // If any of the required methods were not provided, then simply 83 // proxy to the decorated stream. 84 foreach (array_diff(self::SLOTS, array_keys($methods)) as $diff) { 85 /** @var callable $callable */ 86 $callable = [$stream, $diff]; 87 $methods[$diff] = $callable; 88 } 89 90 return new self($methods); 91 } 92 93 public function __toString(): string 94 { 95 try { 96 /** @var string */ 97 return ($this->_fn___toString)(); 98 } catch (\Throwable $e) { 99 if (\PHP_VERSION_ID >= 70400) { 100 throw $e; 101 } 102 trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); 103 104 return ''; 105 } 106 } 107 108 public function close(): void 109 { 110 ($this->_fn_close)(); 111 } 112 113 public function detach() 114 { 115 return ($this->_fn_detach)(); 116 } 117 118 public function getSize(): ?int 119 { 120 return ($this->_fn_getSize)(); 121 } 122 123 public function tell(): int 124 { 125 return ($this->_fn_tell)(); 126 } 127 128 public function eof(): bool 129 { 130 return ($this->_fn_eof)(); 131 } 132 133 public function isSeekable(): bool 134 { 135 return ($this->_fn_isSeekable)(); 136 } 137 138 public function rewind(): void 139 { 140 ($this->_fn_rewind)(); 141 } 142 143 public function seek($offset, $whence = SEEK_SET): void 144 { 145 ($this->_fn_seek)($offset, $whence); 146 } 147 148 public function isWritable(): bool 149 { 150 return ($this->_fn_isWritable)(); 151 } 152 153 public function write($string): int 154 { 155 return ($this->_fn_write)($string); 156 } 157 158 public function isReadable(): bool 159 { 160 return ($this->_fn_isReadable)(); 161 } 162 163 public function read($length): string 164 { 165 return ($this->_fn_read)($length); 166 } 167 168 public function getContents(): string 169 { 170 return ($this->_fn_getContents)(); 171 } 172 173 /** 174 * @return mixed 175 */ 176 public function getMetadata($key = null) 177 { 178 return ($this->_fn_getMetadata)($key); 179 } 180} 181