diff --git a/src/UniformResourceLocator/StreamWrapper/Stream.php b/src/UniformResourceLocator/StreamWrapper/Stream.php index 84aa56e7..3d104cfc 100644 --- a/src/UniformResourceLocator/StreamWrapper/Stream.php +++ b/src/UniformResourceLocator/StreamWrapper/Stream.php @@ -41,7 +41,14 @@ public static function setLocator(ResourceLocatorInterface $locator): void */ public function stream_open(string $uri, string $mode, int $options, ?string &$opened_path): bool { - $path = $this->findPath($uri); + // In write mode, we want to write to the first existing path (should + // be a shared location) because fopen will attempts to create the file. + // Otherwise, we need to find the first found path, across location. + if (in_array($mode, ['w', 'w+', 'a', 'a+', 'x', 'x+'], true)) { + $path = $this->findPath($uri, true); + } else { + $path = $this->findPath($uri); + } if ($path === null) { return false; @@ -49,8 +56,8 @@ public function stream_open(string $uri, string $mode, int $options, ?string &$o $handle = @fopen($path, $mode); - // fopen will return false if mode is 'x' and file already exist. - // See : https://www.php.net/manual/en/function.fopen + // fopen will return false if file is not found or if mode is 'x' and + // file already exist. See : https://www.php.net/manual/en/function.fopen if ($handle === false) { return false; } diff --git a/tests/UniformResourceLocator/StreamWrapper/StreamTest.php b/tests/UniformResourceLocator/StreamWrapper/StreamTest.php index 57b790bd..1059da66 100644 --- a/tests/UniformResourceLocator/StreamWrapper/StreamTest.php +++ b/tests/UniformResourceLocator/StreamWrapper/StreamTest.php @@ -162,17 +162,73 @@ public function testChmod(): void } /** + * Test on a file that already exist. * @depends testSimpleFile */ public function testFOpen(): void + { + touch($this->file); + $this->assertTrue(file_exists($this->file)); // Make sure file exist, it's the point of the test + $this->assertIsResource(fopen($this->file, 'w')); + unlink($this->file); + } + + /** + * Make sure fopen create the file if it doesn't exist. + * @depends testSimpleFile + */ + public function testFOpenFileNotExistButWillCreate(): void + { + $this->assertFalse(file_exists($this->file)); + $this->assertIsResource(fopen($this->file, 'w')); + $this->assertTrue(file_exists($this->file)); + unlink($this->file); + } + + /** + * Make sure fopen will return false if the file doesn't exist in readonly + * mode. Even if locator return a resource path (with all flag). + * + * @depends testSimpleFile + */ + public function testFOpenFileNotExist(): void { // stream_open will trigger an error even if the stream return false. + // fopen(bar://test.txt): Failed to open stream: "UserFrosting\UniformResourceLocator\StreamWrapper\Stream::stream_open" call failed + // This suppress this error, and allow us to test the return value. set_error_handler(function ($no, $str, $file, $line) { // @phpstan-ignore-line }); - touch($this->file); // Touch basic file + $this->assertFalse(file_exists($this->file)); + $this->assertFalse(fopen($this->file, 'r')); + $this->assertFalse(file_exists($this->file)); + } + + /** + * @depends testSimpleFile + */ + public function testFOpenInvalidPath(): void + { + // Catch exception + set_error_handler(function ($no, $str, $file, $line) { // @phpstan-ignore-line + }); + + $this->assertFalse(fopen('bar://test.txt', 'w')); + } + + /** + * fopen will return false if mode is 'x' and file already exist. + * @depends testSimpleFile + */ + public function testFOpenFalseReturn(): void + { + // Catch exception + set_error_handler(function ($no, $str, $file, $line) { // @phpstan-ignore-line + }); + + touch($this->file); $this->assertFalse(fopen($this->file, 'x')); - unlink($this->file); // Reset state + unlink($this->file); } /**