Skip to content

Commit a0fdc31

Browse files
committed
:octocat: +StreamUtil::tryFopen() and tryGetContents()
1 parent fefce50 commit a0fdc31

File tree

2 files changed

+146
-6
lines changed

2 files changed

+146
-6
lines changed

src/StreamUtil.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,15 @@
1414
use Psr\Http\Message\StreamInterface;
1515
use RuntimeException;
1616
use Throwable;
17+
use function fopen;
1718
use function in_array;
1819
use function min;
1920
use function preg_match;
21+
use function restore_error_handler;
22+
use function set_error_handler;
23+
use function sprintf;
2024
use function str_contains;
25+
use function stream_get_contents;
2126
use function strlen;
2227
use function substr;
2328

@@ -139,6 +144,8 @@ public static function getContents(StreamInterface $stream):?string{
139144
*
140145
* Throws if the source is not readable or the destination not writable.
141146
*
147+
* @see https://github.com/guzzle/psr7/blob/815698d9f11c908bc59471d11f642264b533346a/src/Utils.php#L36-L69
148+
*
142149
* @throws \RuntimeException
143150
*/
144151
public static function copyToStream(StreamInterface $source, StreamInterface $destination, int $maxLength = null):int{
@@ -166,4 +173,88 @@ public static function copyToStream(StreamInterface $source, StreamInterface $de
166173
return $bytesRead;
167174
}
168175

176+
/**
177+
* Safely open a PHP resource, throws instead of raising warnings and errors
178+
*
179+
* @see https://github.com/guzzle/psr7/blob/815698d9f11c908bc59471d11f642264b533346a/src/Utils.php#L344-L391
180+
*
181+
* @param string $filename
182+
* @param string $mode
183+
* @param resource|null $context
184+
*
185+
* @return resource
186+
* @throws \RuntimeException
187+
*/
188+
public static function tryFopen(string $filename, string $mode, $context = null){
189+
$exception = null;
190+
$message = 'Unable to open "%s" using mode "%s": %s';
191+
192+
$errorHandler = function(int $errno, string $errstr) use ($filename, $mode, &$exception, $message):bool{
193+
$exception = new RuntimeException(sprintf($message, $filename, $mode, $errstr));
194+
195+
return true;
196+
};
197+
198+
set_error_handler($errorHandler);
199+
200+
try{
201+
/** @var resource $handle */
202+
$handle = fopen(filename: $filename, mode: $mode, context: $context);
203+
}
204+
catch(Throwable $e){
205+
$exception = new RuntimeException(message: sprintf($message, $filename, $mode, $e->getMessage()), previous: $e);
206+
}
207+
208+
restore_error_handler();
209+
210+
if($exception !== null){
211+
throw $exception;
212+
}
213+
214+
return $handle;
215+
}
216+
217+
/**
218+
* Safely get the contents of a stream resource, throws instead of raising warnings and errors
219+
*
220+
* @see https://github.com/guzzle/psr7/blob/815698d9f11c908bc59471d11f642264b533346a/src/Utils.php#L393-L438
221+
*
222+
* @param resource $stream
223+
* @param int|null $length
224+
* @param int $offset
225+
*
226+
* @return string
227+
* @throws \RuntimeException
228+
*/
229+
public static function tryGetContents($stream, int $length = null, int $offset = -1):string{
230+
$exception = null;
231+
$message = 'Unable to read stream contents: %s';
232+
233+
$errorHandler = function(int $errno, string $errstr) use (&$exception, $message):bool{
234+
$exception = new RuntimeException(sprintf($message, $errstr));
235+
236+
return true;
237+
};
238+
239+
set_error_handler($errorHandler);
240+
241+
try{
242+
$contents = stream_get_contents($stream, $length, $offset);
243+
244+
if($contents === false){
245+
$exception = new RuntimeException(sprintf($message, '(returned false)'));
246+
}
247+
248+
}
249+
catch(Throwable $e){
250+
$exception = new RuntimeException(message: sprintf($message, $e->getMessage()), previous: $e);
251+
}
252+
253+
if($exception !== null){
254+
throw $exception;
255+
}
256+
257+
return $contents;
258+
}
259+
169260
}

tests/StreamUtilTest.php

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,11 @@ public function testModeAllowedFlagPositionIrrelevant():void{
6262
$mode = 'rwarrrrrw++++b12345';
6363
$this::assertTrue(StreamUtil::modeAllowsRead($mode));
6464

65-
$fh = fopen(__DIR__.'/fopen-test.txt', $mode);
66-
$meta = stream_get_meta_data($fh);
65+
$resource = fopen(__DIR__.'/fopen-test.txt', $mode);
66+
$meta = stream_get_meta_data($resource);
6767

6868
$this::assertSame(substr($mode, 0, 15), $meta['mode']);
69-
fclose($fh);
69+
fclose($resource);
7070
}
7171

7272
public function testGetContentsRewindsStream():void{
@@ -80,7 +80,8 @@ public function testGetContentsRewindsStream():void{
8080
}
8181

8282
public function testGetContentsFromUnreadableStream():void{
83-
$stream = $this->streamFactory->createStreamFromResource(fopen(__DIR__.'/fopen-test.txt', 'a'));
83+
$resource = fopen(__DIR__.'/fopen-test.txt', 'a');
84+
$stream = $this->streamFactory->createStreamFromResource($resource);
8485

8586
$this::assertFalse($stream->isReadable());
8687
$this::assertNull(StreamUtil::getContents($stream));
@@ -131,10 +132,58 @@ public function testCopyToStreamException():void{
131132
$this->expectException(RuntimeException::class);
132133
$this->expectExceptionMessage('$source must be readable and $destination must be writable');
133134

134-
$streamA = $this->streamFactory->createStreamFromResource(fopen(__DIR__.'/fopen-test.txt', 'a'));
135-
$streamB = $this->streamFactory->createStream();
135+
$resource = fopen(__DIR__.'/fopen-test.txt', 'a');
136+
$streamA = $this->streamFactory->createStreamFromResource($resource);
137+
$streamB = $this->streamFactory->createStream();
136138

137139
StreamUtil::copyToStream($streamA, $streamB);
138140
}
139141

142+
public function testTryFopen():void{
143+
$resource = StreamUtil::tryFopen(__DIR__.'/fopen-test.txt', 'r');
144+
145+
$this::assertIsResource($resource);
146+
147+
fclose($resource);
148+
}
149+
150+
public function testTryFopenThrowsExceptionInsteadOfWarning():void{
151+
$this->expectException(RuntimeException::class);
152+
$this->expectExceptionMessage('Unable to open "/path/not/found" using mode "r": fopen(/path/not/found)');
153+
154+
StreamUtil::tryFopen('/path/not/found', 'r');
155+
}
156+
157+
public function testTryFopenThrowsExceptionInsteadOfValueError():void{
158+
$this->expectException(RuntimeException::class);
159+
$this->expectExceptionMessage('Unable to open "" using mode "r": Path cannot be empty');
160+
161+
StreamUtil::tryFopen('', 'r');
162+
}
163+
164+
public function testTryGetContents():void{
165+
$resource = StreamUtil::tryFopen(__DIR__.'/fopen-test.txt', 'r');
166+
167+
$this::assertStringContainsString('foo', StreamUtil::tryGetContents($resource));
168+
}
169+
170+
public function testTryGetContentsThrowsExceptionOnUnreadableResource():void{
171+
$this->expectException(RuntimeException::class);
172+
$this->expectExceptionMessage('Unable to read stream contents:');
173+
174+
$resource = StreamUtil::tryFopen(__DIR__.'/fopen-test.txt', 'a');
175+
176+
StreamUtil::tryGetContents($resource);
177+
}
178+
179+
public function testTryGetContentsThrowsExceptionOnInvalidResource():void{
180+
$this->expectException(RuntimeException::class);
181+
$this->expectExceptionMessage('supplied resource is not a valid stream resource');
182+
183+
$resource = StreamUtil::tryFopen(__DIR__.'/fopen-test.txt', 'r');
184+
fclose($resource);
185+
186+
StreamUtil::tryGetContents($resource);
187+
}
188+
140189
}

0 commit comments

Comments
 (0)