Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.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   * Reads from multiple streams, one after the other.
  11   *
  12   * This is a read-only stream decorator.
  13   */
  14  final class AppendStream implements StreamInterface
  15  {
  16      /** @var StreamInterface[] Streams being decorated */
  17      private $streams = [];
  18  
  19      /** @var bool */
  20      private $seekable = true;
  21  
  22      /** @var int */
  23      private $current = 0;
  24  
  25      /** @var int */
  26      private $pos = 0;
  27  
  28      /**
  29       * @param StreamInterface[] $streams Streams to decorate. Each stream must
  30       *                                   be readable.
  31       */
  32      public function __construct(array $streams = [])
  33      {
  34          foreach ($streams as $stream) {
  35              $this->addStream($stream);
  36          }
  37      }
  38  
  39      public function __toString(): string
  40      {
  41          try {
  42              $this->rewind();
  43              return $this->getContents();
  44          } catch (\Throwable $e) {
  45              if (\PHP_VERSION_ID >= 70400) {
  46                  throw $e;
  47              }
  48              trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR);
  49              return '';
  50          }
  51      }
  52  
  53      /**
  54       * Add a stream to the AppendStream
  55       *
  56       * @param StreamInterface $stream Stream to append. Must be readable.
  57       *
  58       * @throws \InvalidArgumentException if the stream is not readable
  59       */
  60      public function addStream(StreamInterface $stream): void
  61      {
  62          if (!$stream->isReadable()) {
  63              throw new \InvalidArgumentException('Each stream must be readable');
  64          }
  65  
  66          // The stream is only seekable if all streams are seekable
  67          if (!$stream->isSeekable()) {
  68              $this->seekable = false;
  69          }
  70  
  71          $this->streams[] = $stream;
  72      }
  73  
  74      public function getContents(): string
  75      {
  76          return Utils::copyToString($this);
  77      }
  78  
  79      /**
  80       * Closes each attached stream.
  81       */
  82      public function close(): void
  83      {
  84          $this->pos = $this->current = 0;
  85          $this->seekable = true;
  86  
  87          foreach ($this->streams as $stream) {
  88              $stream->close();
  89          }
  90  
  91          $this->streams = [];
  92      }
  93  
  94      /**
  95       * Detaches each attached stream.
  96       *
  97       * Returns null as it's not clear which underlying stream resource to return.
  98       */
  99      public function detach()
 100      {
 101          $this->pos = $this->current = 0;
 102          $this->seekable = true;
 103  
 104          foreach ($this->streams as $stream) {
 105              $stream->detach();
 106          }
 107  
 108          $this->streams = [];
 109  
 110          return null;
 111      }
 112  
 113      public function tell(): int
 114      {
 115          return $this->pos;
 116      }
 117  
 118      /**
 119       * Tries to calculate the size by adding the size of each stream.
 120       *
 121       * If any of the streams do not return a valid number, then the size of the
 122       * append stream cannot be determined and null is returned.
 123       */
 124      public function getSize(): ?int
 125      {
 126          $size = 0;
 127  
 128          foreach ($this->streams as $stream) {
 129              $s = $stream->getSize();
 130              if ($s === null) {
 131                  return null;
 132              }
 133              $size += $s;
 134          }
 135  
 136          return $size;
 137      }
 138  
 139      public function eof(): bool
 140      {
 141          return !$this->streams ||
 142              ($this->current >= count($this->streams) - 1 &&
 143               $this->streams[$this->current]->eof());
 144      }
 145  
 146      public function rewind(): void
 147      {
 148          $this->seek(0);
 149      }
 150  
 151      /**
 152       * Attempts to seek to the given position. Only supports SEEK_SET.
 153       */
 154      public function seek($offset, $whence = SEEK_SET): void
 155      {
 156          if (!$this->seekable) {
 157              throw new \RuntimeException('This AppendStream is not seekable');
 158          } elseif ($whence !== SEEK_SET) {
 159              throw new \RuntimeException('The AppendStream can only seek with SEEK_SET');
 160          }
 161  
 162          $this->pos = $this->current = 0;
 163  
 164          // Rewind each stream
 165          foreach ($this->streams as $i => $stream) {
 166              try {
 167                  $stream->rewind();
 168              } catch (\Exception $e) {
 169                  throw new \RuntimeException('Unable to seek stream '
 170                      . $i . ' of the AppendStream', 0, $e);
 171              }
 172          }
 173  
 174          // Seek to the actual position by reading from each stream
 175          while ($this->pos < $offset && !$this->eof()) {
 176              $result = $this->read(min(8096, $offset - $this->pos));
 177              if ($result === '') {
 178                  break;
 179              }
 180          }
 181      }
 182  
 183      /**
 184       * Reads from all of the appended streams until the length is met or EOF.
 185       */
 186      public function read($length): string
 187      {
 188          $buffer = '';
 189          $total = count($this->streams) - 1;
 190          $remaining = $length;
 191          $progressToNext = false;
 192  
 193          while ($remaining > 0) {
 194              // Progress to the next stream if needed.
 195              if ($progressToNext || $this->streams[$this->current]->eof()) {
 196                  $progressToNext = false;
 197                  if ($this->current === $total) {
 198                      break;
 199                  }
 200                  $this->current++;
 201              }
 202  
 203              $result = $this->streams[$this->current]->read($remaining);
 204  
 205              if ($result === '') {
 206                  $progressToNext = true;
 207                  continue;
 208              }
 209  
 210              $buffer .= $result;
 211              $remaining = $length - strlen($buffer);
 212          }
 213  
 214          $this->pos += strlen($buffer);
 215  
 216          return $buffer;
 217      }
 218  
 219      public function isReadable(): bool
 220      {
 221          return true;
 222      }
 223  
 224      public function isWritable(): bool
 225      {
 226          return false;
 227      }
 228  
 229      public function isSeekable(): bool
 230      {
 231          return $this->seekable;
 232      }
 233  
 234      public function write($string): int
 235      {
 236          throw new \RuntimeException('Cannot write to an AppendStream');
 237      }
 238  
 239      /**
 240       * {@inheritdoc}
 241       *
 242       * @return mixed
 243       */
 244      public function getMetadata($key = null)
 245      {
 246          return $key ? null : [];
 247      }
 248  }