diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 6cfcb8cb..5cfe54a6 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + @@ -642,9 +642,6 @@ - - - @@ -1147,12 +1144,6 @@ - - - - - - @@ -1167,13 +1158,12 @@ - - - - + + + @@ -1195,37 +1185,11 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - adapter->get($path, $context)]]> @@ -1243,13 +1207,6 @@ - - - - - - - @@ -1576,6 +1533,11 @@ + + + + + @@ -1627,23 +1589,7 @@ - - - - - - - - - - - - - - - - @@ -1654,9 +1600,6 @@ - - - @@ -1671,14 +1614,6 @@ - - - datatime]]> - - - - - @@ -1689,6 +1624,11 @@ + + + + + @@ -1829,9 +1769,6 @@ - - - @@ -1843,6 +1780,11 @@ + + + + + @@ -1889,7 +1831,6 @@ - @@ -2233,6 +2174,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2264,6 +2255,7 @@ + diff --git a/src/SonsOfPHP/Component/Filesystem/Adapter/NativeAdapter.php b/src/SonsOfPHP/Component/Filesystem/Adapter/NativeAdapter.php index a240a596..392a0fa7 100644 --- a/src/SonsOfPHP/Component/Filesystem/Adapter/NativeAdapter.php +++ b/src/SonsOfPHP/Component/Filesystem/Adapter/NativeAdapter.php @@ -4,6 +4,12 @@ namespace SonsOfPHP\Component\Filesystem\Adapter; +use SonsOfPHP\Component\Filesystem\Exception\FileNotFoundException; +use SonsOfPHP\Component\Filesystem\Exception\FilesystemException; +use SonsOfPHP\Component\Filesystem\Exception\UnableToCopyFileException; +use SonsOfPHP\Component\Filesystem\Exception\UnableToDeleteFileException; +use SonsOfPHP\Component\Filesystem\Exception\UnableToMoveFileException; +use SonsOfPHP\Component\Filesystem\Exception\UnableToWriteFileException; use SonsOfPHP\Contract\Filesystem\Adapter\AdapterInterface; use SonsOfPHP\Contract\Filesystem\Adapter\CopyAwareInterface; use SonsOfPHP\Contract\Filesystem\Adapter\DirectoryAwareInterface; @@ -18,25 +24,50 @@ * * @author Joshua Estes */ -final readonly class NativeAdapter implements AdapterInterface, CopyAwareInterface, DirectoryAwareInterface, MoveAwareInterface +final class NativeAdapter implements AdapterInterface, CopyAwareInterface, DirectoryAwareInterface, MoveAwareInterface { public function __construct( private string $prefix, - ) {} + private readonly int $defaultPermissions = 0o777, + ) { + $this->prefix = rtrim($prefix, '/'); + } public function add(string $path, mixed $contents, ?ContextInterface $context = null): void { - file_put_contents($this->prefix . $path, $contents); + if ($this->isFile($path, $context)) { + throw new FilesystemException(sprintf('File "%s" already exists', $path)); + } + + $this->makeDirectory(dirname($path), $context); + + if (false === file_put_contents($this->prefix . '/' . ltrim($path, '/'), $contents)) { + throw new UnableToWriteFileException('Unable to write file "' . $path . '"'); + } + + if (false === chmod($this->prefix . '/' . ltrim($path, '/'), $this->defaultPermissions)) { + throw new FilesystemException('Unable to set permissions on file "' . $path . '"'); + } } public function get(string $path, ?ContextInterface $context = null): mixed { - return file_get_contents($this->prefix . $path); + if (!$this->isFile($path, $context)) { + throw new FileNotFoundException('File "' . $path . '" not found'); + } + + return file_get_contents($this->prefix . '/' . ltrim($path, '/')); } public function remove(string $path, ?ContextInterface $context = null): void { - unlink($this->prefix . $path); + if (!$this->isFile($path, $context)) { + throw new FileNotFoundException('File "' . $path . '" not found'); + } + + if (false === unlink($this->prefix . '/' . ltrim($path, '/'))) { + throw new UnableToDeleteFileException('Unable to remove file "' . $path . '"'); + } } public function has(string $path, ?ContextInterface $context = null): bool @@ -46,21 +77,40 @@ public function has(string $path, ?ContextInterface $context = null): bool public function isFile(string $path, ?ContextInterface $context = null): bool { - return is_file($filename); + return is_file($this->prefix . '/' . ltrim($path, '/')); } public function copy(string $source, string $destination, ?ContextInterface $context = null): void { - copy($this->prefix . $source, $this->prefix . $destination); + if (!$this->isFile($source)) { + throw new FilesystemException('Source file "' . $source . '" does not exist'); + } + + if ($this->isFile($destination)) { + throw new FilesystemException('Destination file "' . $destination . '" already exists'); + } + + if (false === copy($this->prefix . '/' . ltrim($source, '/'), $this->prefix . '/' . ltrim($destination, '/'))) { + throw new UnableToCopyFileException('Unable to file "' . $source . '" to "' . $destination . '"'); + } } public function isDirectory(string $path, ?ContextInterface $context = null): bool { - return is_dir($filename); + return is_dir($this->prefix . '/' . ltrim($path, '/')); + } + + public function makeDirectory(string $path, ?ContextInterface $context = null): void + { + if (!$this->isDirectory($path) && false === mkdir($this->prefix . '/' . ltrim($path, '/'), $this->defaultPermissions, true)) { + throw new FilesystemException('Unable to create directory "' . $path . '"'); + } } public function move(string $source, string $destination, ?ContextInterface $context = null): void { - rename($this->prefix . $source, $this->prefix . $destination); + if (false === rename($this->prefix . '/' . ltrim($source, '/'), $this->prefix . '/' . ltrim($destination, '/'))) { + throw new UnableToMoveFileException('Unable to move file "' . $source . '" to "' . $destination . '"'); + } } } diff --git a/src/SonsOfPHP/Component/Filesystem/Exception/UnableToCopyFileException.php b/src/SonsOfPHP/Component/Filesystem/Exception/UnableToCopyFileException.php index 3c6bb5d0..e7eed902 100644 --- a/src/SonsOfPHP/Component/Filesystem/Exception/UnableToCopyFileException.php +++ b/src/SonsOfPHP/Component/Filesystem/Exception/UnableToCopyFileException.php @@ -4,7 +4,9 @@ namespace SonsOfPHP\Component\Filesystem\Exception; +use SonsOfPHP\Contract\Filesystem\Exception\UnableToCopyFileExceptionInterface; + /** * @author Joshua Estes */ -final class UnableToCopyFileException extends FilesystemException {} +final class UnableToCopyFileException extends FilesystemException implements UnableToCopyFileExceptionInterface {} diff --git a/src/SonsOfPHP/Component/Filesystem/Exception/UnableToDeleteDirectoryException.php b/src/SonsOfPHP/Component/Filesystem/Exception/UnableToDeleteDirectoryException.php index de0f9eec..036f6bb6 100644 --- a/src/SonsOfPHP/Component/Filesystem/Exception/UnableToDeleteDirectoryException.php +++ b/src/SonsOfPHP/Component/Filesystem/Exception/UnableToDeleteDirectoryException.php @@ -4,7 +4,9 @@ namespace SonsOfPHP\Component\Filesystem\Exception; +use SonsOfPHP\Contract\Filesystem\Exception\UnableToDeleteDirectoryExceptionInterface; + /** * @author Joshua Estes */ -final class UnableToDeleteDirectoryException extends FilesystemException {} +final class UnableToDeleteDirectoryException extends FilesystemException implements UnableToDeleteDirectoryExceptionInterface {} diff --git a/src/SonsOfPHP/Component/Filesystem/Exception/UnableToDeleteFileException.php b/src/SonsOfPHP/Component/Filesystem/Exception/UnableToDeleteFileException.php index 7299316e..b7a6605b 100644 --- a/src/SonsOfPHP/Component/Filesystem/Exception/UnableToDeleteFileException.php +++ b/src/SonsOfPHP/Component/Filesystem/Exception/UnableToDeleteFileException.php @@ -4,7 +4,9 @@ namespace SonsOfPHP\Component\Filesystem\Exception; +use SonsOfPHP\Contract\Filesystem\Exception\UnableToDeleteFileExceptionInterface; + /** * @author Joshua Estes */ -final class UnableToDeleteFileException extends FilesystemException {} +final class UnableToDeleteFileException extends FilesystemException implements UnableToDeleteFileExceptionInterface {} diff --git a/src/SonsOfPHP/Component/Filesystem/Exception/UnableToMoveFileException.php b/src/SonsOfPHP/Component/Filesystem/Exception/UnableToMoveFileException.php index abf0e2fd..b17c726a 100644 --- a/src/SonsOfPHP/Component/Filesystem/Exception/UnableToMoveFileException.php +++ b/src/SonsOfPHP/Component/Filesystem/Exception/UnableToMoveFileException.php @@ -4,7 +4,9 @@ namespace SonsOfPHP\Component\Filesystem\Exception; +use SonsOfPHP\Contract\Filesystem\Exception\UnableToMoveFileExceptionInterface; + /** * @author Joshua Estes */ -final class UnableToMoveFileException extends FilesystemException {} +final class UnableToMoveFileException extends FilesystemException implements UnableToMoveFileExceptionInterface {} diff --git a/src/SonsOfPHP/Component/Filesystem/Exception/UnableToReadFileException.php b/src/SonsOfPHP/Component/Filesystem/Exception/UnableToReadFileException.php index 176a7bf2..a65cce10 100644 --- a/src/SonsOfPHP/Component/Filesystem/Exception/UnableToReadFileException.php +++ b/src/SonsOfPHP/Component/Filesystem/Exception/UnableToReadFileException.php @@ -4,7 +4,9 @@ namespace SonsOfPHP\Component\Filesystem\Exception; +use SonsOfPHP\Contract\Filesystem\Exception\UnableToReadFileExceptionInterface; + /** * @author Joshua Estes */ -final class UnableToReadFileException extends FilesystemException {} +final class UnableToReadFileException extends FilesystemException implements UnableToReadFileExceptionInterface {} diff --git a/src/SonsOfPHP/Component/Filesystem/Exception/UnableToWriteFileException.php b/src/SonsOfPHP/Component/Filesystem/Exception/UnableToWriteFileException.php index 5cf4d127..9c32dff5 100644 --- a/src/SonsOfPHP/Component/Filesystem/Exception/UnableToWriteFileException.php +++ b/src/SonsOfPHP/Component/Filesystem/Exception/UnableToWriteFileException.php @@ -4,7 +4,9 @@ namespace SonsOfPHP\Component\Filesystem\Exception; +use SonsOfPHP\Contract\Filesystem\Exception\UnableToWriteFileExceptionInterface; + /** * @author Joshua Estes */ -final class UnableToWriteFileException extends FilesystemException {} +final class UnableToWriteFileException extends FilesystemException implements UnableToWriteFileExceptionInterface {} diff --git a/src/SonsOfPHP/Component/Filesystem/Tests/Adapter/NativeAdapterTest.php b/src/SonsOfPHP/Component/Filesystem/Tests/Adapter/NativeAdapterTest.php index 0d2b8179..41c7f97e 100644 --- a/src/SonsOfPHP/Component/Filesystem/Tests/Adapter/NativeAdapterTest.php +++ b/src/SonsOfPHP/Component/Filesystem/Tests/Adapter/NativeAdapterTest.php @@ -11,14 +11,27 @@ use SonsOfPHP\Contract\Filesystem\Adapter\CopyAwareInterface; use SonsOfPHP\Contract\Filesystem\Adapter\DirectoryAwareInterface; use SonsOfPHP\Contract\Filesystem\Adapter\MoveAwareInterface; +use SonsOfPHP\Contract\Filesystem\Exception\FileNotFoundExceptionInterface; +use SonsOfPHP\Contract\Filesystem\Exception\FilesystemExceptionInterface; -/** - * @internal - * @coversNothing - */ #[CoversClass(NativeAdapter::class)] final class NativeAdapterTest extends TestCase { + private AdapterInterface $adapter; + private string $prefix; + + protected function setUp(): void + { + $this->prefix = sys_get_temp_dir() . '/' . uniqid(); + $this->adapter = new NativeAdapter($this->prefix); + } + + protected function tearDown(): void + { + // @todo Clean up all the files + //rmdir($this->prefix); + } + public function testItHasTheCorrectInterface(): void { $adapter = new NativeAdapter('/tmp'); @@ -28,4 +41,107 @@ public function testItHasTheCorrectInterface(): void $this->assertInstanceOf(DirectoryAwareInterface::class, $adapter); $this->assertInstanceOf(MoveAwareInterface::class, $adapter); } + + public function testItWillWriteFile(): void + { + $path = __FUNCTION__; + $this->assertFalse(is_file($this->prefix . '/' . $path)); + $this->adapter->add($path, 'test'); + $this->assertTrue(is_file($this->prefix . '/' . $path)); + } + + public function testItWillThrowExceptionWhenAddingFileThatExists(): void + { + $path = __FUNCTION__; + $this->assertFalse(is_file($this->prefix . '/' . $path)); + $this->adapter->add($path, 'test'); + $this->assertTrue(is_file($this->prefix . '/' . $path)); + + $this->expectException(FilesystemExceptionInterface::class); + $this->adapter->add($path, 'test'); + } + + public function testItCanReturnTheContentsOfAFile(): void + { + $path = __FUNCTION__; + $this->adapter->add($path, 'test'); + + $this->assertSame('test', $this->adapter->get($path)); + } + + public function testItCanDeleteFileThatExists(): void + { + $path = __FUNCTION__; + $this->adapter->add($path, 'test'); + $this->assertTrue(is_file($this->prefix . '/' . $path)); + + $this->adapter->remove($path); + $this->assertFalse(is_file($this->prefix . '/' . $path)); + } + + public function testItWillReturnCorrectlyForHasWithFile(): void + { + $path = __FUNCTION__; + $this->assertFalse($this->adapter->has($path)); + $this->adapter->add($path, 'test'); + $this->assertTrue($this->adapter->has($path)); + } + + public function testItWillReturnCorrectlyForHasWithDirectory(): void + { + $path = __FUNCTION__; + $this->assertFalse($this->adapter->has($path)); + $this->adapter->makeDirectory($path); + $this->assertTrue($this->adapter->has($path)); + } + + public function testItCanTellTheDifferenceBetweenAFileAndDirectory(): void + { + $dirPath = 'test.d'; + $filePath = 'test'; + $this->adapter->makeDirectory($dirPath); + $this->adapter->add($filePath, 'test'); + + $this->assertFalse($this->adapter->isFile($dirPath)); + $this->assertTrue($this->adapter->isFile($filePath)); + + $this->assertTrue($this->adapter->isDirectory($dirPath)); + $this->assertFalse($this->adapter->isDirectory($filePath)); + } + + public function testItCanCopyAFile(): void + { + $source = 'source.txt'; + $destination = 'destination.txt'; + $this->adapter->add($source, 'test'); + $this->adapter->copy($source, $destination); + $this->assertTrue(is_file($this->prefix . '/' . $source)); + $this->assertTrue(is_file($this->prefix . '/' . $destination)); + $this->assertSame('test', $this->adapter->get($destination)); + } + + public function testItCanMoveAFile(): void + { + $source = 'source.txt'; + $destination = 'destination.txt'; + $this->adapter->add($source, 'test'); + $this->adapter->move($source, $destination); + $this->assertFalse(is_file($this->prefix . '/' . $source)); + $this->assertTrue(is_file($this->prefix . '/' . $destination)); + $this->assertSame('test', $this->adapter->get($destination)); + } + + public function testItWillThrowExceptionWhenFileNotFound(): void + { + $path = __FUNCTION__; + $this->expectException(FileNotFoundExceptionInterface::class); + $this->adapter->get($path); + } + + public function testItWillThrowExceptionWhenDeletingAFileThatDoesNotExist(): void + { + $path = __FUNCTION__; + $this->expectException(FileNotFoundExceptionInterface::class); + $this->adapter->remove($path); + } } diff --git a/src/SonsOfPHP/Contract/Filesystem/Adapter/DirectoryAwareInterface.php b/src/SonsOfPHP/Contract/Filesystem/Adapter/DirectoryAwareInterface.php index 08eeda82..95f001f2 100644 --- a/src/SonsOfPHP/Contract/Filesystem/Adapter/DirectoryAwareInterface.php +++ b/src/SonsOfPHP/Contract/Filesystem/Adapter/DirectoryAwareInterface.php @@ -19,4 +19,14 @@ interface DirectoryAwareInterface * @throws FilesystemExceptionInterface Generic Failure Exception */ public function isDirectory(string $path, ?ContextInterface $context = null): bool; + + /** + * @throws FilesystemExceptionInterface Generic Failure Exception + */ + //public function makeDirectory(string $path, ?ContextInterface $context = null): void; + + /** + * @throws FilesystemExceptionInterface Generic Failure Exception + */ + //public function removeDirectory(string $path, ?ContextInterface $context = null): void; } diff --git a/src/SonsOfPHP/Contract/Filesystem/Exception/UnableToCopyFileExceptionInterface.php b/src/SonsOfPHP/Contract/Filesystem/Exception/UnableToCopyFileExceptionInterface.php new file mode 100644 index 00000000..70826e3a --- /dev/null +++ b/src/SonsOfPHP/Contract/Filesystem/Exception/UnableToCopyFileExceptionInterface.php @@ -0,0 +1,10 @@ + + */ +interface UnableToCopyFileExceptionInterface extends FilesystemExceptionInterface {} diff --git a/src/SonsOfPHP/Contract/Filesystem/Exception/UnableToDeleteDirectoryExceptionInterface.php b/src/SonsOfPHP/Contract/Filesystem/Exception/UnableToDeleteDirectoryExceptionInterface.php new file mode 100644 index 00000000..afdd37be --- /dev/null +++ b/src/SonsOfPHP/Contract/Filesystem/Exception/UnableToDeleteDirectoryExceptionInterface.php @@ -0,0 +1,10 @@ + + */ +interface UnableToDeleteDirectoryExceptionInterface extends FilesystemExceptionInterface {} diff --git a/src/SonsOfPHP/Contract/Filesystem/Exception/UnableToDeleteFileExceptionInterface.php b/src/SonsOfPHP/Contract/Filesystem/Exception/UnableToDeleteFileExceptionInterface.php new file mode 100644 index 00000000..5ec6196f --- /dev/null +++ b/src/SonsOfPHP/Contract/Filesystem/Exception/UnableToDeleteFileExceptionInterface.php @@ -0,0 +1,10 @@ + + */ +interface UnableToDeleteFileExceptionInterface extends FilesystemExceptionInterface {} diff --git a/src/SonsOfPHP/Contract/Filesystem/Exception/UnableToMoveFileExceptionInterface.php b/src/SonsOfPHP/Contract/Filesystem/Exception/UnableToMoveFileExceptionInterface.php new file mode 100644 index 00000000..387623a5 --- /dev/null +++ b/src/SonsOfPHP/Contract/Filesystem/Exception/UnableToMoveFileExceptionInterface.php @@ -0,0 +1,10 @@ + + */ +interface UnableToMoveFileExceptionInterface extends FilesystemExceptionInterface {} diff --git a/src/SonsOfPHP/Contract/Filesystem/Exception/UnableToReadFileExceptionInterface.php b/src/SonsOfPHP/Contract/Filesystem/Exception/UnableToReadFileExceptionInterface.php new file mode 100644 index 00000000..75f659c3 --- /dev/null +++ b/src/SonsOfPHP/Contract/Filesystem/Exception/UnableToReadFileExceptionInterface.php @@ -0,0 +1,10 @@ + + */ +interface UnableToReadFileExceptionInterface extends FilesystemExceptionInterface {} diff --git a/src/SonsOfPHP/Contract/Filesystem/Exception/UnableToWriteFileExceptionInterface.php b/src/SonsOfPHP/Contract/Filesystem/Exception/UnableToWriteFileExceptionInterface.php new file mode 100644 index 00000000..15c8207a --- /dev/null +++ b/src/SonsOfPHP/Contract/Filesystem/Exception/UnableToWriteFileExceptionInterface.php @@ -0,0 +1,10 @@ + + */ +interface UnableToWriteFileExceptionInterface extends FilesystemExceptionInterface {}