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