diff --git a/composer.json b/composer.json index 4714181..59d89b5 100644 --- a/composer.json +++ b/composer.json @@ -43,10 +43,10 @@ "phpunit/phpunit": "^4.7" }, "scripts": { - "test": "vendor/bin/phpunit --colors=always", - "test:acceptance": "vendor/bin/phpunit --colors=always --testsuite acceptance", - "test:functional": "vendor/bin/phpunit --colors=always --testsuite functional", - "test:unit": "vendor/bin/phpunit --colors=always --testsuite unit", + "test": "vendor/bin/phpunit -v --colors=always", + "test:acceptance": "vendor/bin/phpunit -v --colors=always --testsuite acceptance", + "test:functional": "vendor/bin/phpunit -v --colors=always --testsuite functional", + "test:unit": "vendor/bin/phpunit -v --colors=always --testsuite unit", "lint:fix": [ "vendor/bin/php-cs-fixer fix src --level=psr2", "vendor/bin/php-cs-fixer fix test --level=psr2" diff --git a/src/Node/DirectoryLink.php b/src/Node/DirectoryLink.php new file mode 100644 index 0000000..78296ae --- /dev/null +++ b/src/Node/DirectoryLink.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs\Node; + +use DateTime; + +class DirectoryLink implements NodeContainerInterface, LinkInterface +{ + protected $dateAccessed; + protected $dateCreated; + protected $dateModified; + protected $file; + protected $mode; + + /** + * @param NodeContainerInterface $directory + */ + public function __construct(NodeContainerInterface $directory) + { + $this->directory = $directory; + $this->mode = self::TYPE_LINK; + + $this->dateAccessed = new DateTime(); + $this->dateCreated = clone $this->dateAccessed; + $this->dateModified = clone $this->dateAccessed; + } + + public function add($name, NodeInterface $node) + { + $this->directory->add($name, $node); + } + + public function get($name) + { + return $this->directory->get($name); + } + + public function has($name) + { + return $this->directory->has($name); + } + + public function remove($name) + { + $this->directory->remove($name); + } + + public function set($name, NodeInterface $node) + { + $this->directory->set($name, $node); + } + + public function getDateAccessed() + { + return $this->dateAccessed; + } + + public function setDateAccessed(DateTime $dateTime) + { + $this->dateAccessed = $dateTime; + } + + public function getDateCreated() + { + return $this->dateCreated; + } + + public function getDateModified() + { + return $this->dateModified; + } + + public function setDateModified(DateTime $dateTime) + { + $this->dateModified = $dateTime; + } + + public function getIterator() + { + return $this->directory->getIterator(); + } + + public function getMode() + { + return $this->mode; + } + + public function getSize() + { + return $this->directory->getSize(); + } + + public function getTarget() + { + return $this->directory; + } +} diff --git a/src/Node/Factory/NodeFactory.php b/src/Node/Factory/NodeFactory.php index fb8bb8f..ee7370d 100644 --- a/src/Node/Factory/NodeFactory.php +++ b/src/Node/Factory/NodeFactory.php @@ -11,9 +11,11 @@ use LogicException; use Vfs\Node\Directory; +use Vfs\Node\DirectoryLink; use Vfs\Node\File; +use Vfs\Node\FileLink; +use Vfs\Node\FileInterface; use Vfs\Node\NodeContainerInterface; -use Vfs\Node\NodeInterface; class NodeFactory implements NodeFactoryInterface { @@ -22,14 +24,19 @@ public function buildDirectory(array $children = []) return new Directory($children); } + public function buildDirectoryLink(NodeContainerInterface $target) + { + return new DirectoryLink($target); + } + public function buildFile($content = '') { return new File($content); } - public function buildLink($content = '') + public function buildFileLink(FileInterface $target) { - throw new LogicException('Symlinks aren\'t supported yet.'); + return new FileLink($target); } public function buildTree(array $tree) diff --git a/src/Node/Factory/NodeFactoryInterface.php b/src/Node/Factory/NodeFactoryInterface.php index fa498a4..9ab3a05 100644 --- a/src/Node/Factory/NodeFactoryInterface.php +++ b/src/Node/Factory/NodeFactoryInterface.php @@ -9,6 +9,8 @@ */ namespace Vfs\Node\Factory; +use Vfs\Node\FileInterface; +use Vfs\Node\LinkInterface; use Vfs\Node\NodeContainerInterface; use Vfs\Node\NodeInterface; @@ -20,6 +22,12 @@ interface NodeFactoryInterface */ public function buildDirectory(array $children = []); + /** + * @param NodeContainerInterface $target + * @return LinkInterface + */ + public function buildDirectoryLink(NodeContainerInterface $target); + /** * @param string $content * @return NodeInterface @@ -27,10 +35,10 @@ public function buildDirectory(array $children = []); public function buildFile($content = ''); /** - * @param string $content - * @return NodeInterface + * @param FileInterface $file + * @return LinkInterface */ - public function buildLink($content = ''); + public function buildFileLink(FileInterface $target); /** * @param array $tree diff --git a/src/Node/FileLink.php b/src/Node/FileLink.php new file mode 100644 index 0000000..58bff67 --- /dev/null +++ b/src/Node/FileLink.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs\Node; + +use DateTime; + +class FileLink implements FileInterface, LinkInterface +{ + protected $dateAccessed; + protected $dateCreated; + protected $dateModified; + protected $file; + protected $mode; + + /** + * @param FileInterface $file + */ + public function __construct(FileInterface $file) + { + $this->file = $file; + $this->mode = self::TYPE_LINK; + + $this->dateAccessed = new DateTime(); + $this->dateCreated = clone $this->dateAccessed; + $this->dateModified = clone $this->dateAccessed; + } + + public function getContent() + { + return $this->file->getContent(); + } + + public function setContent($content) + { + $this->file->setContent($content); + } + + public function getDateAccessed() + { + return $this->dateAccessed; + } + + public function setDateAccessed(DateTime $dateTime) + { + $this->dateAccessed = $dateTime; + } + + public function getDateCreated() + { + return $this->dateCreated; + } + + public function getDateModified() + { + return $this->dateModified; + } + + public function setDateModified(DateTime $dateTime) + { + $this->dateModified = $dateTime; + } + + public function getMode() + { + return $this->mode; + } + + public function getSize() + { + return $this->file->getSize(); + } + + public function getTarget() + { + return $this->file; + } +} diff --git a/src/Node/LinkInterface.php b/src/Node/LinkInterface.php index e52df13..fd4b63f 100644 --- a/src/Node/LinkInterface.php +++ b/src/Node/LinkInterface.php @@ -9,6 +9,16 @@ */ namespace Vfs\Node; +/** + * Link node type + * + * This is an implementation of a hard link rather than a symbolic link; the + * key difference being it links directly to a node and not a path. If the + * target node is moved or renamed the link remains intact. + * + * The implementation is a simple proxy, whereby most other method calls should + * proxy through to the target where suitable. + */ interface LinkInterface extends NodeInterface { /** diff --git a/src/Node/StatInterface.php b/src/Node/StatInterface.php index 5841af2..afbbd60 100644 --- a/src/Node/StatInterface.php +++ b/src/Node/StatInterface.php @@ -41,6 +41,7 @@ interface StatInterface const S_IWOTH = 0000002; const S_IXOTH = 0000001; + const TYPE_MASK = self::S_IFMT; const TYPE_SOCKET = self::S_IFSOCK; const TYPE_LINK = self::S_IFLNK; const TYPE_FILE = self::S_IFREG; diff --git a/test/acceptance/Stream/StreamWrapper/SymlinkAcceptanceTest.php b/test/acceptance/Stream/StreamWrapper/SymlinkAcceptanceTest.php new file mode 100644 index 0000000..6c92f03 --- /dev/null +++ b/test/acceptance/Stream/StreamWrapper/SymlinkAcceptanceTest.php @@ -0,0 +1,41 @@ + [ + 'bar' => 'baz' + ] + ]; + + public function testIsLink() + { + $factory = $this->fs->getNodeFactory(); + + $file = $this->fs->get('/foo/bar'); + $this->fs->get('/')->add('symlink', $factory->buildFileLink($file)); + + $this->assertTrue(is_link("$this->scheme:///symlink")); + } + + public function testDirectoryLink() + { + $this->markTestSkipped('`symlink()` isn\'t supported by PHP Stream Wrappers'); + + symlink("$this->scheme:///foo/bar", "$this->scheme:///symlink"); + + $this->assertTrue(is_link("$this->scheme:///symlink")); + } + + public function testFileLink() + { + $this->markTestSkipped('`symlink()` isn\'t supported by PHP Stream Wrappers'); + + symlink("$this->scheme:///foo", "$this->scheme:///symlink"); + + $this->assertTrue(is_link("$this->scheme:///symlink")); + } +} diff --git a/test/unit/Node/DirectoryLinkTest.php b/test/unit/Node/DirectoryLinkTest.php new file mode 100644 index 0000000..7a75a6e --- /dev/null +++ b/test/unit/Node/DirectoryLinkTest.php @@ -0,0 +1,118 @@ +nodeA = $a = Mockery::mock('Vfs\Node\NodeInterface'); + $this->nodeB = $b = Mockery::mock('Vfs\Node\NodeInterface'); + $this->nodeC = $c = Mockery::mock('Vfs\Node\NodeInterface'); + $this->nodes = ['foo' => $a, 'bar' => $b, 'baz' => $c]; + } + + public function testInstance() + { + $link = new DirectoryLink(new Directory()); + + $this->assertInstanceOf('Vfs\Node\NodeContainerInterface', $link); + $this->assertInstanceOf('Vfs\Node\LinkInterface', $link); + $this->assertInstanceOf('Vfs\Node\NodeInterface', $link); + $this->assertInstanceOf('Vfs\Node\StatInterface', $link); + } + + public function testAdd() + { + $dir = new Directory(); + $link = new DirectoryLink($dir); + $link->add('foo', $this->nodeA); + + $this->assertSame($this->nodeA, $dir->get('foo')); + } + + public function testGet() + { + $link = new DirectoryLink(new Directory($this->nodes)); + + $this->assertSame($this->nodeA, $link->get('foo')); + } + + public function testGetThrowsMissingNode() + { + $link = new DirectoryLink(new Directory()); + $this->setExpectedException('Vfs\Exception\MissingNodeException'); + + $link->get('foo'); + } + + public function testHasIsTrue() + { + $link = new DirectoryLink(new Directory($this->nodes)); + + $this->assertTrue($link->has('foo')); + } + + public function testHasIsFalse() + { + $link = new DirectoryLink(new Directory()); + + $this->assertFalse($link->has('foo')); + } + + public function testSet() + { + $dir = new Directory(); + $link = new DirectoryLink($dir); + $link->set('foo', $this->nodeA); + + $this->assertSame($this->nodeA, $dir->get('foo')); + } + + public function testGetDateAccessed() + { + $dir = new Directory(); + $link = new DirectoryLink($dir); + + $this->assertInstanceOf('DateTime', $link->getDateAccessed()); + $this->assertNotSame($link->getDateAccessed(), $dir->getDateAccessed()); + } + + public function testGetDateCreated() + { + $dir = new Directory(); + $link = new DirectoryLink($dir); + + $this->assertInstanceOf('DateTime', $link->getDateCreated()); + $this->assertNotSame($link->getDateCreated(), $dir->getDateCreated()); + } + + public function testGetDateModified() + { + $dir = new Directory(); + $link = new DirectoryLink($dir); + + $this->assertInstanceOf('DateTime', $link->getDateModified()); + $this->assertNotSame($link->getDateModified(), $dir->getDateModified()); + } + + public function testGetMode() + { + $link = new DirectoryLink(new Directory()); + + $this->assertEquals(StatInterface::TYPE_LINK, $link->getMode() & StatInterface::TYPE_MASK); + } + + public function testGetSize() + { + $link = new DirectoryLink(new Directory($this->nodes)); + + $this->nodeA->shouldReceive('getSize')->once()->withNoArgs()->andReturn(1); + $this->nodeB->shouldReceive('getSize')->once()->withNoArgs()->andReturn(2); + $this->nodeC->shouldReceive('getSize')->once()->withNoArgs()->andReturn(3); + + $this->assertEquals(6, $link->getSize()); + } +} diff --git a/test/unit/Node/DirectoryTest.php b/test/unit/Node/DirectoryTest.php index 8ca9f69..9d86648 100644 --- a/test/unit/Node/DirectoryTest.php +++ b/test/unit/Node/DirectoryTest.php @@ -120,7 +120,7 @@ public function testGetMode() { $dir = new Directory(); - $this->assertEquals(StatInterface::TYPE_DIR, $dir->getMode() & StatInterface::S_IFMT); + $this->assertEquals(StatInterface::TYPE_DIR, $dir->getMode() & StatInterface::TYPE_MASK); } public function testGetSize() diff --git a/test/unit/Node/Factory/NodeFactoryTest.php b/test/unit/Node/Factory/NodeFactoryTest.php index 5b97492..b67ff61 100644 --- a/test/unit/Node/Factory/NodeFactoryTest.php +++ b/test/unit/Node/Factory/NodeFactoryTest.php @@ -2,6 +2,8 @@ namespace Vfs\Node\Factory; use Mockery; +use Vfs\Node\Directory; +use Vfs\Node\File; use Vfs\Test\UnitTestCase; class NodeFactoryTest extends UnitTestCase @@ -24,6 +26,15 @@ public function testBuildFile() $this->assertEquals('foo', $file->getContent()); } + public function testBuildFileLink() + { + $file = new File(); + $link = $this->factory->buildFileLink($file); + + $this->assertInstanceOf('Vfs\Node\FileInterface', $link); + $this->assertInstanceOf('Vfs\Node\LinkInterface', $link); + } + public function testBuildDirectory() { $node = Mockery::mock('Vfs\Node\NodeInterface'); @@ -33,6 +44,15 @@ public function testBuildDirectory() $this->assertSame($node, $dir->get('foo')); } + public function testBuildDirectoryLink() + { + $dir = new Directory(); + $link = $this->factory->buildDirectoryLink($dir); + + $this->assertInstanceOf('Vfs\Node\NodeContainerInterface', $link); + $this->assertInstanceOf('Vfs\Node\LinkInterface', $link); + } + public function testBuildTree() { $root = $this->factory->buildTree([ diff --git a/test/unit/Node/FileLinkTest.php b/test/unit/Node/FileLinkTest.php new file mode 100644 index 0000000..c117a41 --- /dev/null +++ b/test/unit/Node/FileLinkTest.php @@ -0,0 +1,74 @@ +assertInstanceOf('Vfs\Node\FileInterface', $link); + $this->assertInstanceOf('Vfs\Node\LinkInterface', $link); + $this->assertInstanceOf('Vfs\Node\NodeInterface', $link); + $this->assertInstanceOf('Vfs\Node\StatInterface', $link); + } + + public function testGetContent() + { + $link = new FileLink(new File('foo')); + + $this->assertEquals('foo', $link->getContent()); + } + + public function testSetContent() + { + $file = new File(''); + $link = new FileLink($file); + $link->setContent('foo'); + + $this->assertEquals('foo', $file->getContent()); + } + + public function testGetDateAccessed() + { + $file = new File(); + $link = new FileLink($file); + + $this->assertInstanceOf('DateTime', $link->getDateAccessed()); + $this->assertNotSame($link->getDateAccessed(), $file->getDateAccessed()); + } + + public function testGetDateCreated() + { + $file = new File(); + $link = new FileLink($file); + + $this->assertInstanceOf('DateTime', $link->getDateCreated()); + $this->assertNotSame($link->getDateCreated(), $file->getDateCreated()); + } + + public function testGetDateModified() + { + $file = new File(); + $link = new FileLink($file); + + $this->assertInstanceOf('DateTime', $link->getDateModified()); + $this->assertNotSame($link->getDateModified(), $file->getDateModified()); + } + + public function testGetMode() + { + $link = new FileLink(new File()); + + $this->assertEquals(StatInterface::TYPE_LINK, $link->getMode() & StatInterface::TYPE_MASK); + } + + public function testGetSize() + { + $link = new FileLink(new File('foo')); + + $this->assertEquals(3, $link->getSize()); + } +} diff --git a/test/unit/Node/FileTest.php b/test/unit/Node/FileTest.php index b7b0dee..2cde4ac 100644 --- a/test/unit/Node/FileTest.php +++ b/test/unit/Node/FileTest.php @@ -54,7 +54,7 @@ public function testGetMode() { $file = new File(); - $this->assertEquals(StatInterface::TYPE_FILE, $file->getMode() & StatInterface::S_IFMT); + $this->assertEquals(StatInterface::TYPE_FILE, $file->getMode() & StatInterface::TYPE_MASK); } public function testGetSize()