From adeb6c1bd29e3d1517af5249f4d36c3bfd5d4ed1 Mon Sep 17 00:00:00 2001 From: fabiankohnen Date: Mon, 31 Oct 2022 15:38:55 +0100 Subject: [PATCH 01/15] feat: added rename function --- src/File.php | 13 ++++++++++++- src/FileInterface.php | 2 ++ src/Remote/SftpFile.php | 15 +++++++++++++++ src/Test/FileTest.php | 27 +++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/File.php b/src/File.php index 7b7d5b1..b7817e6 100644 --- a/src/File.php +++ b/src/File.php @@ -14,11 +14,12 @@ class File implements FileInterface protected $fileHandle; protected string $filePath; + protected string $mode; + public function __construct(string $filePath, string $mode) { $this->openStream($filePath, $mode); - $this->filePath = $filePath; } protected function openStream(string $filePath, string $mode): void @@ -34,6 +35,7 @@ protected function openStream(string $filePath, string $mode): void $this->fileHandle = $tmpFileHandle; $this->filePath = $filePath; + $this->mode = $mode; } public function __destruct() @@ -68,4 +70,13 @@ public function getFileHandle() { return $this->fileHandle; } + + public function rename(string $newPath): bool + { + fclose($this->fileHandle); + $success = rename($this->filePath, $newPath); + $this->openStream($newPath, $this->mode); + + return $success; + } } diff --git a/src/FileInterface.php b/src/FileInterface.php index 3d7e97a..ca01176 100644 --- a/src/FileInterface.php +++ b/src/FileInterface.php @@ -19,4 +19,6 @@ public function getBasename(): string; * @return resource */ public function getFileHandle(); + + public function rename(string $newPath): bool; } diff --git a/src/Remote/SftpFile.php b/src/Remote/SftpFile.php index 784714e..c1cdf28 100644 --- a/src/Remote/SftpFile.php +++ b/src/Remote/SftpFile.php @@ -5,6 +5,7 @@ namespace Ambimax\File\Remote; use Ambimax\File\File; +use phpseclib3\Net\SFTP; use phpseclib3\Net\SFTP\Stream; use RuntimeException; @@ -13,12 +14,15 @@ class SftpFile extends File protected string $hostname; protected string $username; protected string $password; + protected SFTP $sftp; public function __construct(string $hostname, string $username, string $password, string $filePath, string $mode) { $this->hostname = $hostname; $this->username = $username; $this->password = $password; + $this->sftp = new SFTP($hostname); + $this->sftp->login($username, $password); parent::__construct($filePath, $mode); } @@ -38,5 +42,16 @@ protected function openStream(string $filePath, string $mode): void } $this->fileHandle = $tmpFileHandle; + $this->filePath = $filePath; + $this->mode = $mode; + } + + public function rename(string $newPath): bool + { + fclose($this->fileHandle); + $success = $this->sftp->rename($this->filePath, $newPath); + $this->openStream($newPath, $this->mode); + + return $success; } } diff --git a/src/Test/FileTest.php b/src/Test/FileTest.php index ba40d8e..02c5a70 100644 --- a/src/Test/FileTest.php +++ b/src/Test/FileTest.php @@ -56,4 +56,31 @@ public function testGetFileHandle(): void $this->assertIsResource($this->readableFile->getFileHandle()); $this->assertIsNotClosedResource($this->readableFile->getFileHandle()); } + + public function testRename(): void + { + $oldFileHandle = $this->readableFile->getFileHandle(); + + $this->readableFile->rename($this->root->url().'/newtest'); + $this->assertSame($this->root->url().'/newtest', $this->readableFile->getPath()); + + $newFileHandle = $this->readableFile->getFileHandle(); + $this->assertSame('testContent', stream_get_contents($newFileHandle)); + + $this->assertIsClosedResource($oldFileHandle); + } + + public function testMoveToDifferentFolder(): void + { + $oldFileHandle = $this->readableFile->getFileHandle(); + + mkdir($this->root->url().'/testFolder'); + $this->readableFile->rename($this->root->url().'/testFolder/newtest'); + $this->assertSame($this->root->url().'/testFolder/newtest', $this->readableFile->getPath()); + + $newFileHandle = $this->readableFile->getFileHandle(); + $this->assertSame('testContent', stream_get_contents($newFileHandle)); + + $this->assertIsClosedResource($oldFileHandle); + } } From 4e349ffab1e0c6f26f86d09d11dd906c1a10a354 Mon Sep 17 00:00:00 2001 From: fabiankohnen Date: Mon, 31 Oct 2022 15:47:07 +0100 Subject: [PATCH 02/15] fix: cs-fix --- src/File.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/File.php b/src/File.php index b7817e6..c9470b7 100644 --- a/src/File.php +++ b/src/File.php @@ -16,7 +16,6 @@ class File implements FileInterface protected string $filePath; protected string $mode; - public function __construct(string $filePath, string $mode) { $this->openStream($filePath, $mode); From 99e639a7d27432cc1385607761c40e0ceb700ee9 Mon Sep 17 00:00:00 2001 From: fabiankohnen Date: Wed, 2 Nov 2022 12:36:59 +0100 Subject: [PATCH 03/15] feat: made rename more predictable --- src/File.php | 5 +++++ src/Remote/SftpFile.php | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/File.php b/src/File.php index c9470b7..393189f 100644 --- a/src/File.php +++ b/src/File.php @@ -72,7 +72,12 @@ public function getFileHandle() public function rename(string $newPath): bool { + if ('/' !== $newPath[0]) { + $newPath = dirname($this->filePath).'/'.$newPath; + } + fclose($this->fileHandle); + $success = rename($this->filePath, $newPath); $this->openStream($newPath, $this->mode); diff --git a/src/Remote/SftpFile.php b/src/Remote/SftpFile.php index c1cdf28..c2bff9a 100644 --- a/src/Remote/SftpFile.php +++ b/src/Remote/SftpFile.php @@ -48,7 +48,12 @@ protected function openStream(string $filePath, string $mode): void public function rename(string $newPath): bool { + if ('/' !== $newPath[0]) { + $newPath = dirname($this->filePath).'/'.$newPath; + } + fclose($this->fileHandle); + $success = $this->sftp->rename($this->filePath, $newPath); $this->openStream($newPath, $this->mode); From 5670633c1abedf67c102be4f6f88243dce5ab6d4 Mon Sep 17 00:00:00 2001 From: fabiankohnen Date: Wed, 2 Nov 2022 12:38:59 +0100 Subject: [PATCH 04/15] feat: added fwrite function and allowed getContent to return the whole content --- src/File.php | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/File.php b/src/File.php index 393189f..b45efb8 100644 --- a/src/File.php +++ b/src/File.php @@ -59,7 +59,24 @@ public function getBasename(): string */ public function getContent() { - return stream_get_contents($this->fileHandle); + $pointerLocation = ftell($this->fileHandle); + if (!$pointerLocation){ + throw new RuntimeException("unable to get current location of the file pointer. exception thrown to prevent unpredictable pointer jumping"); + } + rewind($this->fileHandle); + + $content = stream_get_contents($this->fileHandle); + fseek($this->fileHandle, $pointerLocation); + + return $content; + } + + /** + * @param int<0, max>|null $length + */ + public function fwrite(string $data, ?int $length = null): int|false + { + return fwrite($this->fileHandle, $data, $length); } /** From bb4fe16844ba6de3867a0001d085d15e9056c595 Mon Sep 17 00:00:00 2001 From: fabiankohnen Date: Wed, 2 Nov 2022 12:39:54 +0100 Subject: [PATCH 05/15] feat: added File mode consts --- src/File.php | 7 +++++++ src/Remote/SftpFile.php | 6 ++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/File.php b/src/File.php index b45efb8..10a9679 100644 --- a/src/File.php +++ b/src/File.php @@ -8,6 +8,10 @@ class File implements FileInterface { + public const MODE_READ = 'r'; + public const MODE_READ_PLUS = 'r+'; + public const MODE_WRITE = 'w'; + public const MODE_WRITE_PLUS = 'w+'; /** * @var resource */ @@ -16,6 +20,9 @@ class File implements FileInterface protected string $filePath; protected string $mode; + /** + * @param self::MODE_* $mode + */ public function __construct(string $filePath, string $mode) { $this->openStream($filePath, $mode); diff --git a/src/Remote/SftpFile.php b/src/Remote/SftpFile.php index c2bff9a..84a905b 100644 --- a/src/Remote/SftpFile.php +++ b/src/Remote/SftpFile.php @@ -7,7 +7,6 @@ use Ambimax\File\File; use phpseclib3\Net\SFTP; use phpseclib3\Net\SFTP\Stream; -use RuntimeException; class SftpFile extends File { @@ -16,6 +15,9 @@ class SftpFile extends File protected string $password; protected SFTP $sftp; + /** + * @param File::MODE_* $mode + */ public function __construct(string $hostname, string $username, string $password, string $filePath, string $mode) { $this->hostname = $hostname; @@ -38,7 +40,7 @@ protected function openStream(string $filePath, string $mode): void $mode); if (false === $tmpFileHandle) { - throw new RuntimeException(sprintf('Could not open file \'%s\'.', $filePath)); + throw new \RuntimeException(sprintf('Could not open file \'%s\'.', $filePath)); } $this->fileHandle = $tmpFileHandle; From c2669f5ff02cb7f76db99481b0c06630e975193e Mon Sep 17 00:00:00 2001 From: fabiankohnen Date: Wed, 2 Nov 2022 13:05:12 +0100 Subject: [PATCH 06/15] feat: added fread, ftell and fseek --- src/File.php | 45 ++++++++++++++++++++++++++++--------------- src/FileInterface.php | 15 ++++++++++----- 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/src/File.php b/src/File.php index 10a9679..b3ce6a9 100644 --- a/src/File.php +++ b/src/File.php @@ -61,6 +61,14 @@ public function getBasename(): string return basename($this->filePath); } + /** + * @return resource + */ + public function getFileHandle() + { + return $this->fileHandle; + } + /** * @return false|string */ @@ -78,6 +86,20 @@ public function getContent() return $content; } + public function rename(string $newPath): bool + { + if ('/' !== $newPath[0]) { + $newPath = dirname($this->filePath).'/'.$newPath; + } + + fclose($this->fileHandle); + + $success = rename($this->filePath, $newPath); + $this->openStream($newPath, $this->mode); + + return $success; + } + /** * @param int<0, max>|null $length */ @@ -86,25 +108,18 @@ public function fwrite(string $data, ?int $length = null): int|false return fwrite($this->fileHandle, $data, $length); } - /** - * @return resource - */ - public function getFileHandle() + public function fread(?int $length = null): string|false { - return $this->fileHandle; + return fread($this->fileHandle, $length); } - public function rename(string $newPath): bool + public function ftell(): int|false { - if ('/' !== $newPath[0]) { - $newPath = dirname($this->filePath).'/'.$newPath; - } - - fclose($this->fileHandle); - - $success = rename($this->filePath, $newPath); - $this->openStream($newPath, $this->mode); + return ftell($this->fileHandle); + } - return $success; + public function fseek(int $offset, int $whence = SEEK_SET): int + { + return fseek($this->fileHandle, $offset, $whence); } } diff --git a/src/FileInterface.php b/src/FileInterface.php index ca01176..f358f1a 100644 --- a/src/FileInterface.php +++ b/src/FileInterface.php @@ -6,11 +6,6 @@ interface FileInterface { - /** - * @return false|string - */ - public function getContent(); - public function getPath(): string; public function getBasename(): string; @@ -20,5 +15,15 @@ public function getBasename(): string; */ public function getFileHandle(); + /** + * @return false|string + */ + public function getContent(); + public function rename(string $newPath): bool; + + public function fwrite(string $data, ?int $length = null): int|false; + public function fread(?int $length = null): string|false; + public function ftell(): int|false; + public function fseek(int $offset, int $whence = SEEK_SET): int; } From e0ac105f933238292edec0aa369ef24335edff4f Mon Sep 17 00:00:00 2001 From: fabiankohnen Date: Wed, 2 Nov 2022 15:15:29 +0100 Subject: [PATCH 07/15] fix: fixed type juggling error --- src/File.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/File.php b/src/File.php index b3ce6a9..68d4816 100644 --- a/src/File.php +++ b/src/File.php @@ -75,7 +75,7 @@ public function getFileHandle() public function getContent() { $pointerLocation = ftell($this->fileHandle); - if (!$pointerLocation){ + if ($pointerLocation === false){ throw new RuntimeException("unable to get current location of the file pointer. exception thrown to prevent unpredictable pointer jumping"); } rewind($this->fileHandle); From 6532f5acd426924399de637afd91cbf0c3c84ab1 Mon Sep 17 00:00:00 2001 From: fabiankohnen Date: Thu, 3 Nov 2022 11:13:47 +0100 Subject: [PATCH 08/15] feat: overworked rename method --- src/File.php | 35 +++++++++++++++++++++++++++-------- src/FileInterface.php | 3 +++ src/Remote/SftpFile.php | 4 +--- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/File.php b/src/File.php index 68d4816..f9351eb 100644 --- a/src/File.php +++ b/src/File.php @@ -4,7 +4,7 @@ namespace Ambimax\File; -use RuntimeException; +use Symfony\Component\Filesystem\Path; class File implements FileInterface { @@ -31,12 +31,12 @@ public function __construct(string $filePath, string $mode) protected function openStream(string $filePath, string $mode): void { if (!file_exists($filePath)) { - throw new RuntimeException("File '$filePath' does not exist."); + throw new \RuntimeException("File '$filePath' does not exist."); } $tmpFileHandle = fopen($filePath, $mode); if (false === $tmpFileHandle) { - throw new RuntimeException("Could not open file '$filePath'."); + throw new \RuntimeException("Could not open file '$filePath'."); } $this->fileHandle = $tmpFileHandle; @@ -75,8 +75,8 @@ public function getFileHandle() public function getContent() { $pointerLocation = ftell($this->fileHandle); - if ($pointerLocation === false){ - throw new RuntimeException("unable to get current location of the file pointer. exception thrown to prevent unpredictable pointer jumping"); + if (false === $pointerLocation) { + throw new \RuntimeException('unable to get current location of the file pointer. exception thrown to prevent unpredictable pointer jumping'); } rewind($this->fileHandle); @@ -86,12 +86,31 @@ public function getContent() return $content; } - public function rename(string $newPath): bool + /** + * Changes relative path to an absolut path relative to the current file location. + * This is to prevent confusion of e.g. rename() where the path would be relative to the php working directory. + * + * If the path is already absolute or has a protocol defined the path won't be modified. + * + * @param string $path + * @return string + */ + protected function ensureAbsolutePath(string $path): string { - if ('/' !== $newPath[0]) { - $newPath = dirname($this->filePath).'/'.$newPath; + if ( + true === Path::isLocal($path) && + false === Path::isAbsolute($path) + ) { + return Path::join(dirname($this->filePath), $path); } + return $path; + } + + public function rename(string $newPath): bool + { + $newPath = $this->ensureAbsolutePath($newPath); + fclose($this->fileHandle); $success = rename($this->filePath, $newPath); diff --git a/src/FileInterface.php b/src/FileInterface.php index f358f1a..1f35f9e 100644 --- a/src/FileInterface.php +++ b/src/FileInterface.php @@ -23,7 +23,10 @@ public function getContent(); public function rename(string $newPath): bool; public function fwrite(string $data, ?int $length = null): int|false; + public function fread(?int $length = null): string|false; + public function ftell(): int|false; + public function fseek(int $offset, int $whence = SEEK_SET): int; } diff --git a/src/Remote/SftpFile.php b/src/Remote/SftpFile.php index 84a905b..0e28d36 100644 --- a/src/Remote/SftpFile.php +++ b/src/Remote/SftpFile.php @@ -50,9 +50,7 @@ protected function openStream(string $filePath, string $mode): void public function rename(string $newPath): bool { - if ('/' !== $newPath[0]) { - $newPath = dirname($this->filePath).'/'.$newPath; - } + $newPath = $this->ensureAbsolutePath($newPath); fclose($this->fileHandle); From 18c6d78658e75bddb52819b7df95eee989acc922 Mon Sep 17 00:00:00 2001 From: fabiankohnen Date: Thu, 3 Nov 2022 11:26:54 +0100 Subject: [PATCH 09/15] fix: phpstan --- src/File.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/File.php b/src/File.php index f9351eb..8785d30 100644 --- a/src/File.php +++ b/src/File.php @@ -91,9 +91,6 @@ public function getContent() * This is to prevent confusion of e.g. rename() where the path would be relative to the php working directory. * * If the path is already absolute or has a protocol defined the path won't be modified. - * - * @param string $path - * @return string */ protected function ensureAbsolutePath(string $path): string { @@ -127,6 +124,9 @@ public function fwrite(string $data, ?int $length = null): int|false return fwrite($this->fileHandle, $data, $length); } + /** + * @param int<0, max>|null $length + */ public function fread(?int $length = null): string|false { return fread($this->fileHandle, $length); From 1162444ee5750c015ae5653e709e1a9f82967ad9 Mon Sep 17 00:00:00 2001 From: fabiankohnen Date: Thu, 3 Nov 2022 14:50:22 +0100 Subject: [PATCH 10/15] feat: allow rename to use previous filename if newpath ends with / --- src/File.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/File.php b/src/File.php index 8785d30..311ff97 100644 --- a/src/File.php +++ b/src/File.php @@ -89,8 +89,9 @@ public function getContent() /** * Changes relative path to an absolut path relative to the current file location. * This is to prevent confusion of e.g. rename() where the path would be relative to the php working directory. + * If the path is already absolute or has a protocol defined the path won't be affected by this. * - * If the path is already absolute or has a protocol defined the path won't be modified. + * If the path ends with the directory separator ("/") the current file name will get appended */ protected function ensureAbsolutePath(string $path): string { @@ -98,7 +99,11 @@ protected function ensureAbsolutePath(string $path): string true === Path::isLocal($path) && false === Path::isAbsolute($path) ) { - return Path::join(dirname($this->filePath), $path); + $path = Path::join(dirname($this->filePath), $path); + } + + if ($path[-1] === DIRECTORY_SEPARATOR){ + $path = Path::join($path, $this->getBasename()); } return $path; From 1c21ffe3e3c2cc2ad1b109cf8bbda519353897e1 Mon Sep 17 00:00:00 2001 From: fabiankohnen Date: Thu, 3 Nov 2022 14:51:03 +0100 Subject: [PATCH 11/15] feat: added move as alias for rename --- src/File.php | 5 +++++ src/FileInterface.php | 2 ++ 2 files changed, 7 insertions(+) diff --git a/src/File.php b/src/File.php index 311ff97..e2e2c05 100644 --- a/src/File.php +++ b/src/File.php @@ -121,6 +121,11 @@ public function rename(string $newPath): bool return $success; } + public function move(string $newPath): bool + { + return $this->rename($newPath); + } + /** * @param int<0, max>|null $length */ diff --git a/src/FileInterface.php b/src/FileInterface.php index 1f35f9e..924157b 100644 --- a/src/FileInterface.php +++ b/src/FileInterface.php @@ -22,6 +22,8 @@ public function getContent(); public function rename(string $newPath): bool; + public function move(string $newPath): bool; + public function fwrite(string $data, ?int $length = null): int|false; public function fread(?int $length = null): string|false; From 7657e3381e2c17af111eca49e9ea3164007b7b63 Mon Sep 17 00:00:00 2001 From: fabiankohnen Date: Thu, 3 Nov 2022 14:51:40 +0100 Subject: [PATCH 12/15] test: matched tests to code changes --- src/Test/FileTest.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Test/FileTest.php b/src/Test/FileTest.php index 02c5a70..8427a0e 100644 --- a/src/Test/FileTest.php +++ b/src/Test/FileTest.php @@ -61,7 +61,7 @@ public function testRename(): void { $oldFileHandle = $this->readableFile->getFileHandle(); - $this->readableFile->rename($this->root->url().'/newtest'); + $this->readableFile->rename('newtest'); $this->assertSame($this->root->url().'/newtest', $this->readableFile->getPath()); $newFileHandle = $this->readableFile->getFileHandle(); @@ -70,13 +70,19 @@ public function testRename(): void $this->assertIsClosedResource($oldFileHandle); } - public function testMoveToDifferentFolder(): void + public function testMoveToDifferentFolderAbsolutePath(): void { $oldFileHandle = $this->readableFile->getFileHandle(); mkdir($this->root->url().'/testFolder'); - $this->readableFile->rename($this->root->url().'/testFolder/newtest'); - $this->assertSame($this->root->url().'/testFolder/newtest', $this->readableFile->getPath()); + $this->readableFile->rename($this->root->url().'/testFolder/'); + $this->assertSame($this->root->url().'/testFolder/test', $this->readableFile->getPath()); + $this->assertFileExists($this->root->url().'/testFolder/test'); + + + $this->readableFile->rename('../testFile'); + $this->assertSame($this->root->url().'/testFile', $this->readableFile->getPath()); + $this->assertFileExists($this->root->url().'/testFile'); $newFileHandle = $this->readableFile->getFileHandle(); $this->assertSame('testContent', stream_get_contents($newFileHandle)); From ff5977b15aea724c06913d27dfe5e03026d061e8 Mon Sep 17 00:00:00 2001 From: fabiankohnen Date: Thu, 3 Nov 2022 15:11:56 +0100 Subject: [PATCH 13/15] fix: cs-fix --- src/File.php | 2 +- src/Test/FileTest.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/File.php b/src/File.php index e2e2c05..11baa6e 100644 --- a/src/File.php +++ b/src/File.php @@ -102,7 +102,7 @@ protected function ensureAbsolutePath(string $path): string $path = Path::join(dirname($this->filePath), $path); } - if ($path[-1] === DIRECTORY_SEPARATOR){ + if (DIRECTORY_SEPARATOR === $path[-1]) { $path = Path::join($path, $this->getBasename()); } diff --git a/src/Test/FileTest.php b/src/Test/FileTest.php index 8427a0e..e73ae7e 100644 --- a/src/Test/FileTest.php +++ b/src/Test/FileTest.php @@ -79,7 +79,6 @@ public function testMoveToDifferentFolderAbsolutePath(): void $this->assertSame($this->root->url().'/testFolder/test', $this->readableFile->getPath()); $this->assertFileExists($this->root->url().'/testFolder/test'); - $this->readableFile->rename('../testFile'); $this->assertSame($this->root->url().'/testFile', $this->readableFile->getPath()); $this->assertFileExists($this->root->url().'/testFile'); From 840a1b24f740d74ae626d39342572b812c5d6257 Mon Sep 17 00:00:00 2001 From: fabiankohnen Date: Thu, 3 Nov 2022 15:22:41 +0100 Subject: [PATCH 14/15] feat: added possibility to create file when used with write mode --- src/File.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/File.php b/src/File.php index 11baa6e..5b0bf60 100644 --- a/src/File.php +++ b/src/File.php @@ -30,7 +30,12 @@ public function __construct(string $filePath, string $mode) protected function openStream(string $filePath, string $mode): void { - if (!file_exists($filePath)) { + if ( + !in_array($mode, [ + self::MODE_WRITE, + self::MODE_WRITE_PLUS + ]) && !file_exists($filePath) + ) { throw new \RuntimeException("File '$filePath' does not exist."); } From 60c30da392464c638e64810f9dfcacd5671277ed Mon Sep 17 00:00:00 2001 From: fabiankohnen Date: Thu, 3 Nov 2022 15:46:06 +0100 Subject: [PATCH 15/15] fix: cs-fix --- src/File.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/File.php b/src/File.php index 5b0bf60..b0c0650 100644 --- a/src/File.php +++ b/src/File.php @@ -33,7 +33,7 @@ protected function openStream(string $filePath, string $mode): void if ( !in_array($mode, [ self::MODE_WRITE, - self::MODE_WRITE_PLUS + self::MODE_WRITE_PLUS, ]) && !file_exists($filePath) ) { throw new \RuntimeException("File '$filePath' does not exist.");