Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
   1  <?php
   2  
   3  declare(strict_types=1);
   4  
   5  namespace GuzzleHttp\Psr7;
   6  
   7  use Psr\Http\Message\StreamInterface;
   8  
   9  /**
  10   * PHP stream implementation.
  11   */
  12  class Stream implements StreamInterface
  13  {
  14      /**
  15       * @see http://php.net/manual/function.fopen.php
  16       * @see http://php.net/manual/en/function.gzopen.php
  17       */
  18      private const READABLE_MODES = '/r|a\+|ab\+|w\+|wb\+|x\+|xb\+|c\+|cb\+/';
  19      private const WRITABLE_MODES = '/a|w|r\+|rb\+|rw|x|c/';
  20  
  21      /** @var resource */
  22      private $stream;
  23      /** @var int|null */
  24      private $size;
  25      /** @var bool */
  26      private $seekable;
  27      /** @var bool */
  28      private $readable;
  29      /** @var bool */
  30      private $writable;
  31      /** @var string|null */
  32      private $uri;
  33      /** @var mixed[] */
  34      private $customMetadata;
  35  
  36      /**
  37       * This constructor accepts an associative array of options.
  38       *
  39       * - size: (int) If a read stream would otherwise have an indeterminate
  40       *   size, but the size is known due to foreknowledge, then you can
  41       *   provide that size, in bytes.
  42       * - metadata: (array) Any additional metadata to return when the metadata
  43       *   of the stream is accessed.
  44       *
  45       * @param resource                            $stream  Stream resource to wrap.
  46       * @param array{size?: int, metadata?: array} $options Associative array of options.
  47       *
  48       * @throws \InvalidArgumentException if the stream is not a stream resource
  49       */
  50      public function __construct($stream, array $options = [])
  51      {
  52          if (!is_resource($stream)) {
  53              throw new \InvalidArgumentException('Stream must be a resource');
  54          }
  55  
  56          if (isset($options['size'])) {
  57              $this->size = $options['size'];
  58          }
  59  
  60          $this->customMetadata = $options['metadata'] ?? [];
  61          $this->stream = $stream;
  62          $meta = stream_get_meta_data($this->stream);
  63          $this->seekable = $meta['seekable'];
  64          $this->readable = (bool)preg_match(self::READABLE_MODES, $meta['mode']);
  65          $this->writable = (bool)preg_match(self::WRITABLE_MODES, $meta['mode']);
  66          $this->uri = $this->getMetadata('uri');
  67      }
  68  
  69      /**
  70       * Closes the stream when the destructed
  71       */
  72      public function __destruct()
  73      {
  74          $this->close();
  75      }
  76  
  77      public function __toString(): string
  78      {
  79          try {
  80              if ($this->isSeekable()) {
  81                  $this->seek(0);
  82              }
  83              return $this->getContents();
  84          } catch (\Throwable $e) {
  85              if (\PHP_VERSION_ID >= 70400) {
  86                  throw $e;
  87              }
  88              trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR);
  89              return '';
  90          }
  91      }
  92  
  93      public function getContents(): string
  94      {
  95          if (!isset($this->stream)) {
  96              throw new \RuntimeException('Stream is detached');
  97          }
  98  
  99          if (!$this->readable) {
 100              throw new \RuntimeException('Cannot read from non-readable stream');
 101          }
 102  
 103          return Utils::tryGetContents($this->stream);
 104      }
 105  
 106      public function close(): void
 107      {
 108          if (isset($this->stream)) {
 109              if (is_resource($this->stream)) {
 110                  fclose($this->stream);
 111              }
 112              $this->detach();
 113          }
 114      }
 115  
 116      public function detach()
 117      {
 118          if (!isset($this->stream)) {
 119              return null;
 120          }
 121  
 122          $result = $this->stream;
 123          unset($this->stream);
 124          $this->size = $this->uri = null;
 125          $this->readable = $this->writable = $this->seekable = false;
 126  
 127          return $result;
 128      }
 129  
 130      public function getSize(): ?int
 131      {
 132          if ($this->size !== null) {
 133              return $this->size;
 134          }
 135  
 136          if (!isset($this->stream)) {
 137              return null;
 138          }
 139  
 140          // Clear the stat cache if the stream has a URI
 141          if ($this->uri) {
 142              clearstatcache(true, $this->uri);
 143          }
 144  
 145          $stats = fstat($this->stream);
 146          if (is_array($stats) && isset($stats['size'])) {
 147              $this->size = $stats['size'];
 148              return $this->size;
 149          }
 150  
 151          return null;
 152      }
 153  
 154      public function isReadable(): bool
 155      {
 156          return $this->readable;
 157      }
 158  
 159      public function isWritable(): bool
 160      {
 161          return $this->writable;
 162      }
 163  
 164      public function isSeekable(): bool
 165      {
 166          return $this->seekable;
 167      }
 168  
 169      public function eof(): bool
 170      {
 171          if (!isset($this->stream)) {
 172              throw new \RuntimeException('Stream is detached');
 173          }
 174  
 175          return feof($this->stream);
 176      }
 177  
 178      public function tell(): int
 179      {
 180          if (!isset($this->stream)) {
 181              throw new \RuntimeException('Stream is detached');
 182          }
 183  
 184          $result = ftell($this->stream);
 185  
 186          if ($result === false) {
 187              throw new \RuntimeException('Unable to determine stream position');
 188          }
 189  
 190          return $result;
 191      }
 192  
 193      public function rewind(): void
 194      {
 195          $this->seek(0);
 196      }
 197  
 198      public function seek($offset, $whence = SEEK_SET): void
 199      {
 200          $whence = (int) $whence;
 201  
 202          if (!isset($this->stream)) {
 203              throw new \RuntimeException('Stream is detached');
 204          }
 205          if (!$this->seekable) {
 206              throw new \RuntimeException('Stream is not seekable');
 207          }
 208          if (fseek($this->stream, $offset, $whence) === -1) {
 209              throw new \RuntimeException('Unable to seek to stream position '
 210                  . $offset . ' with whence ' . var_export($whence, true));
 211          }
 212      }
 213  
 214      public function read($length): string
 215      {
 216          if (!isset($this->stream)) {
 217              throw new \RuntimeException('Stream is detached');
 218          }
 219          if (!$this->readable) {
 220              throw new \RuntimeException('Cannot read from non-readable stream');
 221          }
 222          if ($length < 0) {
 223              throw new \RuntimeException('Length parameter cannot be negative');
 224          }
 225  
 226          if (0 === $length) {
 227              return '';
 228          }
 229  
 230          try {
 231              $string = fread($this->stream, $length);
 232          } catch (\Exception $e) {
 233              throw new \RuntimeException('Unable to read from stream', 0, $e);
 234          }
 235  
 236          if (false === $string) {
 237              throw new \RuntimeException('Unable to read from stream');
 238          }
 239  
 240          return $string;
 241      }
 242  
 243      public function write($string): int
 244      {
 245          if (!isset($this->stream)) {
 246              throw new \RuntimeException('Stream is detached');
 247          }
 248          if (!$this->writable) {
 249              throw new \RuntimeException('Cannot write to a non-writable stream');
 250          }
 251  
 252          // We can't know the size after writing anything
 253          $this->size = null;
 254          $result = fwrite($this->stream, $string);
 255  
 256          if ($result === false) {
 257              throw new \RuntimeException('Unable to write to stream');
 258          }
 259  
 260          return $result;
 261      }
 262  
 263      /**
 264       * {@inheritdoc}
 265       *
 266       * @return mixed
 267       */
 268      public function getMetadata($key = null)
 269      {
 270          if (!isset($this->stream)) {
 271              return $key ? null : [];
 272          } elseif (!$key) {
 273              return $this->customMetadata + stream_get_meta_data($this->stream);
 274          } elseif (isset($this->customMetadata[$key])) {
 275              return $this->customMetadata[$key];
 276          }
 277  
 278          $meta = stream_get_meta_data($this->stream);
 279  
 280          return $meta[$key] ?? null;
 281      }
 282  }