diff --git a/src/Symfony/Component/Config/Resource/DirectoryResource.php b/src/Symfony/Component/Config/Resource/DirectoryResource.php index 5ccd204ef9..474fbb3078 100644 --- a/src/Symfony/Component/Config/Resource/DirectoryResource.php +++ b/src/Symfony/Component/Config/Resource/DirectoryResource.php @@ -16,7 +16,7 @@ namespace Symfony\Component\Config\Resource; * * @author Fabien Potencier */ -class DirectoryResource implements ResourceInterface, \Serializable +class DirectoryResource implements ResourceInterface { private $resource; private $pattern; @@ -33,6 +33,82 @@ class DirectoryResource implements ResourceInterface, \Serializable $this->pattern = $pattern; } + /** + * Returns the list of filtered file and directory childs of directory resource. + * + * @return array An array of files + */ + public function getFilteredChilds() + { + if (!$this->exists()) { + return array(); + } + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($this->resource, \FilesystemIterator::SKIP_DOTS), + \RecursiveIteratorIterator::SELF_FIRST + ); + + $childs = array(); + foreach ($iterator as $file) { + // if regex filtering is enabled only return matching files + if ($file->isFile() && !$this->hasFile($file)) { + continue; + } + + // always monitor directories for changes, except the .. entries + // (otherwise deleted files wouldn't get detected) + if ($file->isDir() && '/..' === substr($file, -3)) { + continue; + } + + $childs[] = $file; + } + + return $childs; + } + + /** + * Returns child resources that matches directory filters. + * + * @return array + */ + public function getFilteredResources() + { + if (!$this->exists()) { + return array(); + } + + $iterator = new \DirectoryIterator($this->resource); + + $resources = array(); + foreach ($iterator as $file) { + // if regex filtering is enabled only return matching files + if ($file->isFile() && !$this->hasFile($file)) { + continue; + } + + // always monitor directories for changes, except the .. entries + // (otherwise deleted files wouldn't get detected) + if ($file->isDir() && '/..' === substr($file, -3)) { + continue; + } + + // if file is dot - continue + if ($file->isDot()) { + continue; + } + + if ($file->isFile()) { + $resources[] = new FileResource($file->getRealPath()); + } elseif ($file->isDir()) { + $resources[] = new DirectoryResource($file->getRealPath()); + } + } + + return $resources; + } + /** * Returns a string representation of the Resource. * @@ -53,11 +129,62 @@ class DirectoryResource implements ResourceInterface, \Serializable return $this->resource; } + /** + * Returns check pattern. + * + * @return mixed + */ public function getPattern() { return $this->pattern; } + /** + * Checks that passed file exists in resource and matches resource filters. + * + * @param SplFileInfo|string $file + * + * @return Boolean + */ + public function hasFile($file) + { + if (!$file instanceof \SplFileInfo) { + $file = new \SplFileInfo($file); + } + + if (0 !== strpos($file->getRealPath(), realpath($this->resource))) { + return false; + } + + if ($this->pattern) { + return (bool) preg_match($this->pattern, $file->getBasename()); + } + + return true; + } + + /** + * Returns resource mtime. + * + * @return integer + */ + public function getModificationTime() + { + if (!$this->exists()) { + return -1; + } + + clearstatcache(true, $this->resource); + $newestMTime = filemtime($this->resource); + + foreach ($this->getFilteredChilds() as $file) { + clearstatcache(true, (string) $file); + $newestMTime = max($file->getMTime(), $newestMTime); + } + + return $newestMTime; + } + /** * Returns true if the resource has not been updated since the given timestamp. * @@ -67,27 +194,31 @@ class DirectoryResource implements ResourceInterface, \Serializable */ public function isFresh($timestamp) { - if (!is_dir($this->resource)) { + if (!$this->exists()) { return false; } - $newestMTime = filemtime($this->resource); - foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->resource), \RecursiveIteratorIterator::SELF_FIRST) as $file) { - // if regex filtering is enabled only check matching files - if ($this->pattern && $file->isFile() && !preg_match($this->pattern, $file->getBasename())) { - continue; - } + return $this->getModificationTime() < $timestamp; + } - // always monitor directories for changes, except the .. entries - // (otherwise deleted files wouldn't get detected) - if ($file->isDir() && '/..' === substr($file, -3)) { - continue; - } + /** + * Returns true if the resource exists in the filesystem. + * + * @return Boolean + */ + public function exists() + { + return is_dir($this->resource); + } - $newestMTime = max($file->getMTime(), $newestMTime); - } - - return $newestMTime < $timestamp; + /** + * Returns unique resource ID. + * + * @return string + */ + public function getId() + { + return md5('d'.$this->resource.$this->pattern); } public function serialize() diff --git a/src/Symfony/Component/Config/Resource/FileResource.php b/src/Symfony/Component/Config/Resource/FileResource.php index 619f84bcef..259db3c75d 100644 --- a/src/Symfony/Component/Config/Resource/FileResource.php +++ b/src/Symfony/Component/Config/Resource/FileResource.php @@ -18,7 +18,7 @@ namespace Symfony\Component\Config\Resource; * * @author Fabien Potencier */ -class FileResource implements ResourceInterface, \Serializable +class FileResource implements ResourceInterface { private $resource; @@ -29,7 +29,7 @@ class FileResource implements ResourceInterface, \Serializable */ public function __construct($resource) { - $this->resource = realpath($resource); + $this->resource = file_exists($resource) ? realpath($resource) : $resource; } /** @@ -52,6 +52,22 @@ class FileResource implements ResourceInterface, \Serializable return $this->resource; } + /** + * Returns resource mtime. + * + * @return integer + */ + public function getModificationTime() + { + if (!$this->exists()) { + return -1; + } + + clearstatcache(true, $this->resource); + + return filemtime($this->resource); + } + /** * Returns true if the resource has not been updated since the given timestamp. * @@ -61,11 +77,31 @@ class FileResource implements ResourceInterface, \Serializable */ public function isFresh($timestamp) { - if (!file_exists($this->resource)) { + if (!$this->exists()) { return false; } - return filemtime($this->resource) < $timestamp; + return $this->getModificationTime() <= $timestamp; + } + + /** + * Returns true if the resource exists in the filesystem. + * + * @return Boolean + */ + public function exists() + { + return is_file($this->resource); + } + + /** + * Returns unique resource ID. + * + * @return string + */ + public function getId() + { + return md5('f'.$this->resource); } public function serialize() diff --git a/src/Symfony/Component/Config/Resource/ResourceInterface.php b/src/Symfony/Component/Config/Resource/ResourceInterface.php index 024f2e95f9..7febbd8139 100644 --- a/src/Symfony/Component/Config/Resource/ResourceInterface.php +++ b/src/Symfony/Component/Config/Resource/ResourceInterface.php @@ -16,7 +16,7 @@ namespace Symfony\Component\Config\Resource; * * @author Fabien Potencier */ -interface ResourceInterface +interface ResourceInterface extends \Serializable { /** * Returns a string representation of the Resource. @@ -34,10 +34,31 @@ interface ResourceInterface */ function isFresh($timestamp); + /** + * Returns resource mtime. + * + * @return integer + */ + function getModificationTime(); + + /** + * Returns true if the resource exists in the filesystem. + * + * @return Boolean + */ + function exists(); + /** * Returns the resource tied to this Resource. * * @return mixed The resource */ function getResource(); + + /** + * Returns unique resource ID. + * + * @return string + */ + function getId(); } diff --git a/src/Symfony/Component/Config/Tests/Resource/DirectoryResourceTest.php b/src/Symfony/Component/Config/Tests/Resource/DirectoryResourceTest.php index c626ec62ad..d702a5bc55 100644 --- a/src/Symfony/Component/Config/Tests/Resource/DirectoryResourceTest.php +++ b/src/Symfony/Component/Config/Tests/Resource/DirectoryResourceTest.php @@ -50,6 +50,20 @@ class DirectoryResourceTest extends \PHPUnit_Framework_TestCase rmdir($directory); } + /** + * @covers Symfony\Component\Config\Resource\DirectoryResource::getId + */ + public function testGetId() + { + $resource1 = new DirectoryResource($this->directory); + $resource2 = new DirectoryResource($this->directory); + $resource3 = new DirectoryResource($this->directory, '/\.(foo|xml)$/'); + + $this->assertNotNull($resource1->getId()); + $this->assertEquals($resource1->getId(), $resource2->getId()); + $this->assertNotEquals($resource1->getId(), $resource3->getId()); + } + /** * @covers Symfony\Component\Config\Resource\DirectoryResource::getResource */ @@ -168,4 +182,102 @@ class DirectoryResourceTest extends \PHPUnit_Framework_TestCase touch($this->directory.'/new.xml', time() + 20); $this->assertFalse($resource->isFresh(time() + 10), '->isFresh() returns false if an new file matching the filter regex is created '); } + + /** + * @covers Symfony\Component\Config\Resource\DirectoryResource::hasFile + */ + public function testHasFile() + { + $resource = new DirectoryResource($this->directory, '/\.foo$/'); + + touch($this->directory.'/new.foo', time() + 20); + + $this->assertFalse($resource->hasFile($this->directory.'/tmp.xml')); + $this->assertTrue($resource->hasFile($this->directory.'/new.foo')); + } + + /** + * @covers Symfony\Component\Config\Resource\DirectoryResource::getFilteredChilds + */ + public function testGetFilteredChilds() + { + $resource = new DirectoryResource($this->directory, '/\.(foo|xml)$/'); + + touch($file1 = $this->directory.'/new.xml', time() + 20); + touch($file2 = $this->directory.'/old.foo', time() + 20); + touch($this->directory.'/old', time() + 20); + mkdir($dir = $this->directory.'/sub'); + touch($file3 = $this->directory.'/sub/file.foo', time() + 20); + + $childs = $resource->getFilteredChilds(); + $this->assertSame(5, count($childs)); + + $childs = array_map(function($item) { + return (string) $item; + }, $childs); + + $this->assertContains($file1, $childs); + $this->assertContains($file2, $childs); + $this->assertContains($dir, $childs); + $this->assertContains($this->directory.'/tmp.xml', $childs); + $this->assertContains($file3, $childs); + } + + /** + * @covers Symfony\Component\Config\Resource\DirectoryResource::getFilteredResources + */ + public function testGetFilteredResources() + { + $resource = new DirectoryResource($this->directory, '/\.(foo|xml)$/'); + + touch($file1 = $this->directory.'/new.xml', time() + 20); + touch($file2 = $this->directory.'/old.foo', time() + 20); + touch($this->directory.'/old', time() + 20); + mkdir($dir = $this->directory.'/sub'); + touch($file3 = $this->directory.'/sub/file.foo', time() + 20); + + $resources = $resource->getFilteredResources(); + $this->assertSame(4, count($resources)); + + $childs = array_map(function($item) { + return realpath($item->getResource()); + }, $resources); + + $this->assertContains(realpath($file1), $childs); + $this->assertContains(realpath($file2), $childs); + $this->assertContains(realpath($dir), $childs); + $this->assertContains(realpath($this->directory.'/tmp.xml'), $childs); + } + + /** + * @covers Symfony\Component\Config\Resource\DirectoryResource::exists + */ + public function testDirectoryExists() + { + $resource = new DirectoryResource($this->directory); + + $this->assertTrue($resource->exists(), '->exists() returns true if directory exists '); + + unlink($this->directory.'/tmp.xml'); + rmdir($this->directory); + + $this->assertFalse($resource->exists(), '->exists() returns false if directory does not exists'); + } + + /** + * @covers Symfony\Component\Config\Resource\DirectoryResource::getModificationTime + */ + public function testGetModificationTime() + { + $resource = new DirectoryResource($this->directory, '/\.(foo|xml)$/'); + + touch($this->directory.'/new.xml', $time = time() + 20); + $this->assertSame($time, $resource->getModificationTime(), '->getModificationTime() returns time of the last modificated resource'); + + touch($this->directory.'/some', time() + 60); + $this->assertSame($time, $resource->getModificationTime(), '->getModificationTime() returns time of last modificated resource, that only matches pattern'); + + touch($this->directory, $time2 = time() + 90); + $this->assertSame($time2, $resource->getModificationTime(), '->getModificationTime() returns modification time of the directory itself'); + } } diff --git a/src/Symfony/Component/Config/Tests/Resource/FileResourceTest.php b/src/Symfony/Component/Config/Tests/Resource/FileResourceTest.php index 83c403bbe7..5413c7ed70 100644 --- a/src/Symfony/Component/Config/Tests/Resource/FileResourceTest.php +++ b/src/Symfony/Component/Config/Tests/Resource/FileResourceTest.php @@ -27,7 +27,21 @@ class FileResourceTest extends \PHPUnit_Framework_TestCase protected function tearDown() { - unlink($this->file); + if ($this->file) { + unlink($this->file); + } + } + + /** + * @covers Symfony\Component\Config\Resource\DirectoryResource::getId + */ + public function testGetId() + { + $resource1 = new FileResource($this->file); + $resource2 = new FileResource($this->file); + + $this->assertNotNull($resource1->getId()); + $this->assertEquals($resource1->getId(), $resource2->getId()); } /** @@ -49,4 +63,26 @@ class FileResourceTest extends \PHPUnit_Framework_TestCase $resource = new FileResource('/____foo/foobar'.rand(1, 999999)); $this->assertFalse($resource->isFresh(time()), '->isFresh() returns false if the resource does not exist'); } + + /** + * @covers Symfony\Component\Config\Resource\FileResource::getModificationTime + */ + public function testGetModificationTime() + { + touch($this->file, $time = time() + 100); + $this->assertSame($time, $this->resource->getModificationTime()); + } + + /** + * @covers Symfony\Component\Config\Resource\FileResource::exists + */ + public function testExists() + { + $this->assertTrue($this->resource->exists(), '->exists() returns true if the resource exists'); + + unlink($this->file); + $this->file = null; + + $this->assertFalse($this->resource->exists(), '->exists() returns false if the resource does not exists'); + } }