FnStream.php 4.4 KB

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