├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── docs └── index.md ├── mkdocs.yml ├── phpunit.xml.dist ├── src └── Lurker │ ├── Event │ └── FilesystemEvent.php │ ├── Exception │ ├── ExceptionInterface.php │ ├── InvalidArgumentException.php │ └── RuntimeException.php │ ├── Resource │ ├── DirectoryResource.php │ ├── FileResource.php │ ├── ResourceInterface.php │ └── TrackedResource.php │ ├── ResourceWatcher.php │ ├── StateChecker │ ├── DirectoryStateChecker.php │ ├── FileStateChecker.php │ ├── Inotify │ │ ├── CheckerBag.php │ │ ├── DirectoryStateChecker.php │ │ ├── FileStateChecker.php │ │ ├── NewDirectoryStateChecker.php │ │ ├── ResourceStateChecker.php │ │ └── TopDirectoryStateChecker.php │ ├── NewDirectoryStateChecker.php │ ├── ResourceStateChecker.php │ └── StateCheckerInterface.php │ └── Tracker │ ├── InotifyTracker.php │ ├── RecursiveIteratorTracker.php │ └── TrackerInterface.php └── tests ├── Lurker └── Tests │ ├── Event │ └── FilesystemEventTest.php │ ├── ResourceWatcherTest.php │ ├── StateChecker │ ├── DirectoryStateCheckerTest.php │ ├── FileStateCheckerTest.php │ └── Inotify │ │ ├── DirectoryStateCheckerTest.php │ │ ├── FileStateCheckerTest.php │ │ ├── Fixtures │ │ ├── DirectoryStateCheckerForTest.php │ │ ├── FileStateCheckerForTest.php │ │ └── TopDirectoryStateCheckerForTest.php │ │ ├── StateCheckerTest.php │ │ └── TopDirectoryStateCheckerTest.php │ └── Tracker │ ├── InotifyTrackerTest.php │ ├── RecursiveIteratorTrackerTest.php │ └── TrackerTest.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | phpunit.xml 3 | composer.lock 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: false 4 | 5 | 6 | cache: 7 | directories: 8 | - $HOME/.composer/cache/files 9 | 10 | php: 11 | - 5.4 12 | - 5.5 13 | - 5.6 14 | - 7.0 15 | - hhvm 16 | 17 | matrix: 18 | include: 19 | - php: 5.4 20 | env: INOTIFY_EXTENSION=1 21 | - php: 5.5 22 | env: INOTIFY_EXTENSION=1 23 | - php: 5.6 24 | env: INOTIFY_EXTENSION=1 25 | 26 | before_script: 27 | - "if [ \"$INOTIFY_EXTENSION\" = \"1\" ]; then pyrus install pecl/inotify && pyrus build pecl/inotify; fi" 28 | - "if [ \"$INOTIFY_EXTENSION\" = \"1\" ]; then echo \"extension=inotify.so\" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi" 29 | - "composer install --no-progress" 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 1.0.1 (xxxx-xx-xx) 5 | ------------------ 6 | 7 | * Typo and examples fixes. 8 | * Change DirectoryResource::getModificationTime to return the modified time of the directory itself. 9 | * Fix warnings and exceptions that could be thrown in some race conditions. 10 | 11 | 1.0.0 (2013-07-04) 12 | ------------------ 13 | 14 | * First tagged version, extract Symfony Resource Watcher as Lurker. 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Konstantin Kudryashov & Henrik Bjornskov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Lurker 2 | ====== 3 | 4 | Resource tracking for PHP. Watch files and/or directories. For more information 5 | [look at the documentation here](http://lurker.rtfd.org). 6 | 7 | [![Build Status](https://travis-ci.org/flint/Lurker.png?branch=master)](https://travis-ci.org/flint/Lurker) 8 | 9 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "henrikbjorn/lurker", 3 | "type" : "library", 4 | "description" : "Resource Watcher.", 5 | "keywords" : ["resource", "filesystem", "watching"], 6 | "license" : "MIT", 7 | 8 | "authors" : [ 9 | { 10 | "name" : "Konstantin Kudryashov", 11 | "email" : "ever.zet@gmail.com" 12 | }, 13 | { 14 | "name" : "Yaroslav Kiliba", 15 | "email" : "om.dattaya@gmail.com" 16 | }, 17 | { 18 | "name" : "Henrik Bjrnskov", 19 | "email" : "henrik@bjrnskov.dk" 20 | } 21 | ], 22 | 23 | "require" : { 24 | "php" : ">=5.3.3", 25 | "symfony/config" : "^2.2|^3.0", 26 | "symfony/event-dispatcher" : "^2.2|^3.0" 27 | }, 28 | 29 | "autoload" : { 30 | "psr-0" : { "Lurker": "src" } 31 | }, 32 | 33 | "suggest" : { 34 | "ext-inotify": ">=0.1.6" 35 | }, 36 | 37 | "extra" : { 38 | "branch-alias" : { 39 | "dev-master" : "1.0.x-dev" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | Getting Started 2 | =============== 3 | 4 | Use composer to install it by adding the following to your `composer.json` file. 5 | 6 | ``` bash 7 | composer require henrikbjorn/lurker 8 | ``` 9 | 10 | And then run `composer update henrikbjorn/lurker` to get the package installed. 11 | 12 | Tracking Resources 13 | ------------------ 14 | 15 | Lurker works by giving the resource watcher a tracking id which is the name of the event and a path to 16 | the resource you want to track. 17 | 18 | When all the resources have been added that should be track you would want to add event listeners for them so 19 | your can act when the resources are changed. 20 | 21 | ``` php 22 | track('twig.templates', '/path/to/views'); 29 | 30 | $watcher->addListener('twig.templates', function (FilesystemEvent $event) { 31 | echo $event->getResource() . 'was' . $event->getTypeString(); 32 | }); 33 | 34 | $watcher->start(); 35 | ``` 36 | 37 | The above example would watch for all events `create`, `delete` and `modify`. This can be controlled by passing a 38 | third parameter to `track()`. 39 | 40 | ``` php 41 | track('twig.templates', '/path/to/views', FilesystemEvent::CREATE); 44 | $watcher->track('twig.templates', '/path/to/views', FilesystemEvent::MODIFY); 45 | $watcher->track('twig.templates', '/path/to/views', FilesystemEvent::DELETE); 46 | $watcher->track('twig.templates', '/path/to/views', FilesystemEvent::ALL); 47 | ``` 48 | 49 | Note that `FilesystemEvent::ALL` is a special case and of course means it will watch for every type of event. 50 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Lurker 2 | theme: readthedocs 3 | repo_url: https://github.com/flint/Lurker 4 | 5 | markdown_extensions: 6 | - toc: 7 | permalink: true 8 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | 20 | 21 | src 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Lurker/Event/FilesystemEvent.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class FilesystemEvent extends Event 18 | { 19 | const CREATE = 1; 20 | const MODIFY = 2; 21 | const DELETE = 4; 22 | const ALL = 7; 23 | 24 | private $tracked; 25 | private $resource; 26 | private $type; 27 | 28 | protected static $types = array( 29 | 1 => 'create', 30 | 2 => 'modify', 31 | 4 => 'delete', 32 | ); 33 | 34 | /** 35 | * Initializes resource event. 36 | * 37 | * @param TrackedResource $tracked resource, that being tracked 38 | * @param ResourceInterface $resource resource instance 39 | * @param integer $type event type bit 40 | */ 41 | public function __construct(TrackedResource $tracked, ResourceInterface $resource, $type) 42 | { 43 | if (!isset(self::$types[$type])) { 44 | throw new InvalidArgumentException('Wrong event type providen'); 45 | } 46 | 47 | $this->tracked = $tracked; 48 | $this->resource = $resource; 49 | $this->type = $type; 50 | } 51 | 52 | /** 53 | * Returns resource, that being tracked while event occured. 54 | * 55 | * @return integer 56 | */ 57 | public function getTrackedResource() 58 | { 59 | return $this->tracked; 60 | } 61 | 62 | /** 63 | * Returns changed resource. 64 | * 65 | * @return ResourceInterface 66 | */ 67 | public function getResource() 68 | { 69 | return $this->resource; 70 | } 71 | 72 | /** 73 | * Returns true is resource, that fired event is file. 74 | * 75 | * @return Boolean 76 | */ 77 | public function isFileChange() 78 | { 79 | return $this->resource instanceof FileResource; 80 | } 81 | 82 | /** 83 | * Returns true is resource, that fired event is directory. 84 | * 85 | * @return Boolean 86 | */ 87 | public function isDirectoryChange() 88 | { 89 | return $this->resource instanceof DirectoryResource; 90 | } 91 | 92 | /** 93 | * Returns event type. 94 | * 95 | * @return integer 96 | */ 97 | public function getType() 98 | { 99 | return $this->type; 100 | } 101 | 102 | /** 103 | * Returns event type string representation. 104 | * 105 | * @return string 106 | */ 107 | public function getTypeString() 108 | { 109 | return self::$types[$this->getType()]; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Lurker/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface ExceptionInterface 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Lurker/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class InvalidArgumentException extends BaseInvalidArgumentException implements ExceptionInterface 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Lurker/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class RuntimeException extends BaseRuntimeException implements ExceptionInterface 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Lurker/Resource/DirectoryResource.php: -------------------------------------------------------------------------------- 1 | getResource()); 16 | 17 | return is_dir($resource); 18 | } 19 | 20 | public function getModificationTime() 21 | { 22 | if (!$this->exists()) { 23 | return -1; 24 | } 25 | 26 | clearstatcache(true, $this->getResource()); 27 | if (false === $mtime = @filemtime($this->getResource())) { 28 | return -1; 29 | } 30 | 31 | return $mtime; 32 | } 33 | 34 | public function isFresh($timestamp) 35 | { 36 | if (!$this->exists()) { 37 | return false; 38 | } 39 | 40 | return $this->getModificationTime() < $timestamp; 41 | } 42 | 43 | public function getId() 44 | { 45 | return md5('d' . $this . $this->getPattern()); 46 | } 47 | 48 | public function hasFile($file) 49 | { 50 | if (!$file instanceof \SplFileInfo) { 51 | $file = new \SplFileInfo($file); 52 | } 53 | 54 | if (0 !== strpos($file->getRealPath(), realpath($this->getResource()))) { 55 | return false; 56 | } 57 | 58 | if ($this->getPattern()) { 59 | return (bool) preg_match($this->getPattern(), $file->getBasename()); 60 | } 61 | 62 | return true; 63 | } 64 | 65 | public function getFilteredResources() 66 | { 67 | if (!$this->exists()) { 68 | return array(); 69 | } 70 | 71 | // race conditions 72 | try { 73 | $iterator = new \DirectoryIterator($this->getResource()); 74 | } catch (\UnexpectedValueException $e) { 75 | return array(); 76 | } 77 | 78 | $resources = array(); 79 | foreach ($iterator as $file) { 80 | // if regex filtering is enabled only return matching files 81 | if ($file->isFile() && !$this->hasFile($file)) { 82 | continue; 83 | } 84 | 85 | // always monitor directories for changes, except the .. entries 86 | // (otherwise deleted files wouldn't get detected) 87 | if ($file->isDir() && '/..' === substr($file, -3)) { 88 | continue; 89 | } 90 | 91 | // if file is dot - continue 92 | if ($file->isDot()) { 93 | continue; 94 | } 95 | 96 | if ($file->isFile()) { 97 | $resources[] = new FileResource($file->getRealPath()); 98 | } elseif ($file->isDir()) { 99 | $resources[] = new DirectoryResource($file->getRealPath()); 100 | } 101 | } 102 | 103 | return $resources; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Lurker/Resource/FileResource.php: -------------------------------------------------------------------------------- 1 | exists()) { 15 | return -1; 16 | } 17 | 18 | clearstatcache(true, $this->getResource()); 19 | if (false === $mtime = @filemtime($this->getResource())) { 20 | return -1; 21 | } 22 | 23 | return $mtime; 24 | } 25 | 26 | public function getId() 27 | { 28 | return md5('f' . $this); 29 | } 30 | 31 | public function isFresh($timestamp) 32 | { 33 | if (!$this->exists()) { 34 | return false; 35 | } 36 | 37 | return $this->getModificationTime() < $timestamp; 38 | } 39 | 40 | public function exists() 41 | { 42 | clearstatcache(true, $this->getResource()); 43 | 44 | return is_file($this); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Lurker/Resource/ResourceInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class TrackedResource 13 | { 14 | private $trackingId; 15 | private $resource; 16 | 17 | /** 18 | * Initializes tracked resource. 19 | * 20 | * @param string $trackingId id of the tracked resource 21 | * @param ResourceInterface $resource resource 22 | */ 23 | public function __construct($trackingId, ResourceInterface $resource) 24 | { 25 | if (!$resource->exists()) { 26 | throw new InvalidArgumentException(sprintf( 27 | 'Unable to track a non-existent resource (%s)', $resource 28 | )); 29 | } 30 | 31 | $this->trackingId = $trackingId; 32 | $this->resource = $resource; 33 | } 34 | 35 | /** 36 | * Returns tracking ID of the resource. 37 | * 38 | * @return string 39 | */ 40 | public function getTrackingId() 41 | { 42 | return $this->trackingId; 43 | } 44 | 45 | /** 46 | * Returns original resource instance. 47 | * 48 | * @return ResourceInterface 49 | */ 50 | public function getOriginalResource() 51 | { 52 | return $this->resource; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Lurker/ResourceWatcher.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class ResourceWatcher 23 | { 24 | private $tracker; 25 | private $eventDispatcher; 26 | private $watching = false; 27 | 28 | /** 29 | * Initializes path watcher. 30 | * 31 | * @param TrackerInterface $tracker 32 | * @param EventDispatcherInterface $eventDispatcher 33 | */ 34 | public function __construct(TrackerInterface $tracker = null, EventDispatcherInterface $eventDispatcher = null) 35 | { 36 | if (null === $tracker) { 37 | if (function_exists('inotify_init')) { 38 | $tracker = new InotifyTracker(); 39 | } else { 40 | $tracker = new RecursiveIteratorTracker(); 41 | } 42 | } 43 | 44 | if (null === $eventDispatcher) { 45 | $eventDispatcher = new EventDispatcher(); 46 | } 47 | 48 | $this->tracker = $tracker; 49 | $this->eventDispatcher = $eventDispatcher; 50 | } 51 | 52 | /** 53 | * Returns current tracker instance. 54 | * 55 | * @return TrackerInterface 56 | */ 57 | public function getTracker() 58 | { 59 | return $this->tracker; 60 | } 61 | 62 | /** 63 | * Returns event dispatcher mapped to this tracker. 64 | * 65 | * @return EventDispatcherInterface 66 | */ 67 | public function getEventDispatcher() 68 | { 69 | return $this->eventDispatcher; 70 | } 71 | 72 | /** 73 | * Track resource with watcher. 74 | * 75 | * @param string $trackingId id to this track (used for events naming) 76 | * @param ResourceInterface|string $resource resource to track 77 | * @param integer $eventsMask event types bitmask 78 | * 79 | * @throws InvalidArgumentException If 'all' is used as a tracking id 80 | */ 81 | public function track($trackingId, $resource, $eventsMask = FilesystemEvent::ALL) 82 | { 83 | if ('all' === $trackingId) { 84 | throw new InvalidArgumentException( 85 | '"all" is a reserved keyword and can not be used as tracking id' 86 | ); 87 | } 88 | 89 | if (!$resource instanceof ResourceInterface) { 90 | if (is_file($resource)) { 91 | $resource = new FileResource($resource); 92 | } elseif (is_dir($resource)) { 93 | $resource = new DirectoryResource($resource); 94 | } else { 95 | throw new InvalidArgumentException(sprintf( 96 | 'Second argument to track() should be either file or directory resource, '. 97 | 'but got "%s"', 98 | $resource 99 | )); 100 | } 101 | } 102 | 103 | $trackedResource = new TrackedResource($trackingId, $resource); 104 | $this->getTracker()->track($trackedResource, $eventsMask); 105 | } 106 | 107 | /** 108 | * Adds callback as specific tracking listener. 109 | * 110 | * @param string $trackingId id to this track (used for events naming) 111 | * @param callable $callback callback to call on change 112 | * 113 | * @throws InvalidArgumentException If $callback argument isn't callable 114 | */ 115 | public function addListener($trackingId, $callback) 116 | { 117 | if (!is_callable($callback)) { 118 | throw new InvalidArgumentException(sprintf( 119 | 'Second argument to listen() should be callable, but got %s', gettype($callback) 120 | )); 121 | } 122 | 123 | $this->getEventDispatcher()->addListener('resource_watcher.'.$trackingId, $callback); 124 | } 125 | 126 | /** 127 | * Tracks specific resource change by provided callback. 128 | * 129 | * @param ResourceInterface|string $resource resource to track 130 | * @param callable $callback callback to call on change 131 | * @param integer $eventsMask event types bitmask 132 | */ 133 | public function trackByListener($resource, $callback, $eventsMask = FilesystemEvent::ALL) 134 | { 135 | $this->track($trackingId = md5((string) $resource.$eventsMask), $resource, $eventsMask); 136 | $this->addListener($trackingId, $callback); 137 | } 138 | 139 | /** 140 | * Returns true if watcher is currently watching on tracked resources (started). 141 | * 142 | * @return Boolean 143 | */ 144 | public function isWatching() 145 | { 146 | return $this->watching; 147 | } 148 | 149 | /** 150 | * Starts watching on tracked resources. 151 | * 152 | * @param integer $checkInterval check interval in microseconds 153 | * @param integer $timeLimit maximum watching time limit in microseconds 154 | */ 155 | public function start($checkInterval = 1000000, $timeLimit = null) 156 | { 157 | $totalTime = 0; 158 | $this->watching = true; 159 | 160 | while ($this->watching) { 161 | usleep($checkInterval); 162 | $totalTime += $checkInterval; 163 | 164 | if (null !== $timeLimit && $totalTime > $timeLimit) { 165 | break; 166 | } 167 | 168 | foreach ($this->getTracker()->getEvents() as $event) { 169 | $trackedResource = $event->getTrackedResource(); 170 | 171 | // fire global event 172 | $this->getEventDispatcher()->dispatch( 173 | 'resource_watcher.all', 174 | $event 175 | ); 176 | 177 | // fire specific trackingId event 178 | $this->getEventDispatcher()->dispatch( 179 | sprintf('resource_watcher.%s', $trackedResource->getTrackingId()), 180 | $event 181 | ); 182 | } 183 | } 184 | 185 | $this->watching = false; 186 | } 187 | 188 | /** 189 | * Stop watching on tracked resources. 190 | */ 191 | public function stop() 192 | { 193 | $this->watching = false; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/Lurker/StateChecker/DirectoryStateChecker.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class DirectoryStateChecker extends NewDirectoryStateChecker 14 | { 15 | /** 16 | * {@inheritdoc} 17 | */ 18 | public function __construct(DirectoryResource $resource, $eventsMask = FilesystemEvent::ALL) 19 | { 20 | parent::__construct($resource, $eventsMask); 21 | 22 | foreach ($this->createDirectoryChildCheckers($resource) as $checker) { 23 | $this->childs[$checker->getResource()->getId()] = $checker; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Lurker/StateChecker/FileStateChecker.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class FileStateChecker extends ResourceStateChecker 14 | { 15 | /** 16 | * Initializes checker. 17 | * 18 | * @param FileResource $resource 19 | * @param integer $eventsMask event types bitmask 20 | */ 21 | public function __construct(FileResource $resource, $eventsMask = FilesystemEvent::ALL) 22 | { 23 | parent::__construct($resource, $eventsMask); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Lurker/StateChecker/Inotify/CheckerBag.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class CheckerBag 11 | { 12 | /** 13 | * @var \SplObjectStorage[] 14 | */ 15 | protected $watched = array(); 16 | 17 | /** 18 | * @var resource Inotify resource. 19 | */ 20 | private $inotify; 21 | 22 | /** 23 | * Initializes bag. 24 | * 25 | * @param resource $inotify Inotify resource 26 | */ 27 | public function __construct($inotify) 28 | { 29 | $this->inotify = $inotify; 30 | } 31 | 32 | /** 33 | * Adds state checker to the bag. 34 | * 35 | * @param ResourceStateChecker $watched 36 | */ 37 | public function add(ResourceStateChecker $watched) 38 | { 39 | $id = $watched->getId(); 40 | if (!isset($this->watched[$id])) { 41 | $this->watched[$id] = new \SplObjectStorage(); 42 | } 43 | 44 | $this->watched[$id]->attach($watched); 45 | } 46 | 47 | /** 48 | * Returns state checker from the bag 49 | * 50 | * @param int $id Watch descriptor 51 | * 52 | * @return \SplObjectStorage|array 53 | */ 54 | public function get($id) 55 | { 56 | return isset($this->watched[$id]) ? $this->watched[$id] : array(); 57 | } 58 | 59 | /** 60 | * Checks whether at least one state checker with id $id exists. 61 | * 62 | * @param int $id Watch descriptor 63 | * 64 | * @return bool 65 | */ 66 | public function has($id) 67 | { 68 | return isset($this->watched[$id]) && 0 !== $this->watched[$id]->count(); 69 | } 70 | 71 | /** 72 | * @return resource Inotify resource 73 | */ 74 | public function getInotify() 75 | { 76 | return $this->inotify; 77 | } 78 | 79 | /** 80 | * Removes state checker from the bag 81 | * 82 | * @param ResourceStateChecker $watched 83 | */ 84 | public function remove(ResourceStateChecker $watched) 85 | { 86 | $this->watched[$watched->getId()]->detach($watched); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Lurker/StateChecker/Inotify/DirectoryStateChecker.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class DirectoryStateChecker extends ResourceStateChecker 15 | { 16 | /** 17 | * @var DirectoryStateChecker[] 18 | */ 19 | protected $directories = array(); 20 | 21 | /** 22 | * @var FileResource[] 23 | */ 24 | protected $files = array(); 25 | 26 | /** 27 | * @var array File inotify events 28 | */ 29 | protected $fileEvents = array(); 30 | 31 | /** 32 | * @var array Dir inotify events 33 | */ 34 | protected $dirEvents = array(); 35 | 36 | /** 37 | * @var array It is used to track resource moving 38 | * @see DirectoryStateChecker::trackMoveEvent() 39 | */ 40 | protected $movedResources = array(); 41 | 42 | /** 43 | * @var string Key in the $movedResources array where to put name of the resource from next following move event. 44 | * @see DirectoryStateChecker::trackMoveEvent() 45 | */ 46 | protected $lastMove; 47 | 48 | /** 49 | * @var bool 50 | */ 51 | protected $isNew = false; 52 | 53 | /** 54 | * Initializes checker. 55 | * 56 | * @param CheckerBag $bag 57 | * @param DirectoryResource $resource 58 | * @param int $eventsMask 59 | */ 60 | public function __construct(CheckerBag $bag, DirectoryResource $resource, $eventsMask = FilesystemEvent::ALL) 61 | { 62 | parent::__construct($bag, $resource, $eventsMask); 63 | 64 | $this->createChildCheckers(); 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | public function setEvent($mask, $name = '') 71 | { 72 | if ($this->isDir($mask)) { 73 | if (0 !== (IN_ATTRIB & $mask)) { 74 | return; 75 | } 76 | $this->dirEvents[$name] = $mask; 77 | } else { 78 | $this->fileEvents[$name] = $mask; 79 | } 80 | $this->trackMoveEvent($mask, $name); 81 | } 82 | 83 | /** 84 | * {@inheritdoc} 85 | */ 86 | public function getChangeset() 87 | { 88 | $this->event = isset($this->fileEvents['']) ? $this->fileEvents[''] : null; 89 | unset($this->fileEvents['']); 90 | 91 | $this->handleItself(); 92 | 93 | $changeset = array(); 94 | if ($this->event) { 95 | if ($event = $this->fromInotifyMask($this->event)) { 96 | $changeset[] = array( 97 | 'resource' => $this->getResource(), 98 | 'event' => $event 99 | ); 100 | } 101 | 102 | if ($this->isDeleted($this->event)) { 103 | $this->fileEvents = array_fill_keys(array_keys($this->files), IN_DELETE); 104 | $this->dirEvents = array_fill_keys(array_keys($this->directories), IN_DELETE); 105 | $this->getBag()->remove($this); 106 | $this->id = null; 107 | } 108 | } 109 | $deleted = array(); 110 | 111 | foreach ($this->movedResources as $key => $value) { 112 | if ($key === $value) { 113 | unset($this->dirEvents[$key]); 114 | unset($this->fileEvents[$key]); 115 | } 116 | } 117 | 118 | foreach ($this->dirEvents as $name => $event) { 119 | $normalized = $this->normalizeEvent($event); 120 | if (isset($this->directories[$name])) { 121 | $this->directories[$name]->setEvent($normalized); 122 | if ($this->isDeleted($normalized)) { 123 | $deleted[] = $this->directories[$name]; 124 | unset($this->directories[$name]); 125 | } 126 | } elseif (!$this->isDeleted($normalized)) { 127 | $this->createNewDirectoryChecker($name); 128 | } 129 | } 130 | 131 | foreach ($this->fileEvents as $name => $event) { 132 | $normalized = $this->normalizeFileEvent($event, $name); 133 | if (($event = $this->fromInotifyMask($normalized)) && $this->files[$name] instanceof FileResource) { 134 | $changeset[] = 135 | array( 136 | 'resource' => $this->files[$name], 137 | 'event' => $event 138 | ); 139 | } 140 | if ($this->isDeleted($normalized)) { 141 | unset($this->files[$name]); 142 | } 143 | } 144 | 145 | $funct = function($checker) use (&$changeset) { 146 | foreach ($checker->getChangeset() as $change) { 147 | $changeset[] = $change; 148 | } 149 | }; 150 | 151 | array_walk($this->directories, $funct); 152 | array_walk($deleted, $funct); 153 | 154 | $this->dirEvents = $this->fileEvents = $this->movedResources = array(); 155 | $this->event = null; 156 | 157 | return $changeset; 158 | } 159 | 160 | /** 161 | * Tracks move event. It is for situation when resource was roundtripped, e.g. 162 | * rename('dir', 'dir_new'); rename('dir_new', 'dir'). As a result no events should be returned. 163 | * This function just keeps track of the move events, and they're analyzed in the getChangeset method. 164 | * 165 | * @param int $mask 166 | * @param string $name 167 | */ 168 | protected function trackMoveEvent($mask, $name) 169 | { 170 | if ($this->isMovedFrom($mask)) { 171 | if ($key = array_search($name, $this->movedResources)) { 172 | $this->lastMove = $key; 173 | } else { 174 | $this->lastMove = $name; 175 | } 176 | } elseif ($this->isMovedTo($mask)) { 177 | $this->movedResources[$this->lastMove] = $name; 178 | } elseif ($key = array_search($name, $this->movedResources)) { 179 | unset($this->movedResources[$key]); 180 | } 181 | } 182 | 183 | /** 184 | * Handles event related to itself. 185 | */ 186 | protected function handleItself() 187 | { 188 | if (!$this->isNew && $this->isCreated($this->event)) { 189 | $this->unwatch($this->id); 190 | $this->reindexChildCheckers(); 191 | $this->event = null; 192 | } 193 | $this->isNew = false; 194 | } 195 | 196 | /** 197 | * Reads files and subdirectories and transforms them to resources. 198 | */ 199 | protected function createChildCheckers() 200 | { 201 | foreach ($this->getResource()->getFilteredResources() as $resource) { 202 | $resource instanceof DirectoryResource 203 | ? $this->directories[basename((string) $resource)] = new DirectoryStateChecker($this->getBag(), $resource, $this->getEventsMask()) 204 | : $this->files[basename((string) $resource)] = $resource; 205 | } 206 | } 207 | 208 | /** 209 | * Used in case the folder was deleted and than created again or situations like this. 210 | * It rescans the folder, files that was before get IN_MODIFY event, folders - IN_CREATE - to make them to rescan itself 211 | */ 212 | protected function reindexChildCheckers() 213 | { 214 | $this->fileEvents = array_fill_keys(array_keys($this->files), IN_DELETE); 215 | $this->dirEvents = array_fill_keys(array_keys($this->directories), IN_DELETE); 216 | foreach ($this->getResource()->getFilteredResources() as $resource) { 217 | $basename = basename((string) $resource); 218 | if ($resource instanceof FileResource) { 219 | if (isset($this->files[$basename])) { 220 | $this->fileEvents[$basename] = IN_MODIFY; 221 | } else { 222 | $this->files[$basename] = $resource; 223 | $this->fileEvents[$basename] = 'new'; 224 | } 225 | } else { 226 | isset($this->directories[$basename]) 227 | ? $this->dirEvents[$basename] = IN_CREATE 228 | : $this->createNewDirectoryChecker($basename, $resource); 229 | } 230 | } 231 | $this->watch(); 232 | } 233 | 234 | /** 235 | * Normalizes file event 236 | * 237 | * @param int $event 238 | * @param string $name 239 | * 240 | * @return null|int 241 | */ 242 | protected function normalizeFileEvent($event, $name) 243 | { 244 | if ('new' === $event) { 245 | return IN_CREATE; 246 | } 247 | 248 | $event = $this->normalizeEvent($event); 249 | if (isset($this->files[$name])) { 250 | return $this->isCreated($event) ? IN_MODIFY : $event; 251 | } 252 | if (!$this->isDeleted($event)) { 253 | $this->createFileResource($name); 254 | 255 | return IN_CREATE; 256 | } 257 | 258 | return null; 259 | } 260 | 261 | /** 262 | * Normalizes event 263 | * 264 | * @param int $event 265 | * 266 | * @return int 267 | */ 268 | protected function normalizeEvent($event) 269 | { 270 | $event &= ~IN_ISDIR; 271 | if (0 !== ($event & IN_MOVED_FROM)) { 272 | return IN_DELETE; 273 | } elseif (0 !== ($event & IN_MOVED_TO)) { 274 | return IN_CREATE; 275 | } 276 | 277 | return $event; 278 | } 279 | 280 | /** 281 | * Creates new DirectoryStateChecker 282 | * 283 | * @param string $name 284 | * @param null|DirectoryResource $resource 285 | */ 286 | protected function createNewDirectoryChecker($name, DirectoryResource $resource = null) 287 | { 288 | $resource = $resource ?: new DirectoryResource($this->getResource()->getResource().'/'.$name); 289 | $this->directories[$name] = new NewDirectoryStateChecker($this->getBag(), $resource, $this->getEventsMask()); 290 | } 291 | 292 | /** 293 | * Creates new FileResource 294 | * 295 | * @param string $name 296 | */ 297 | protected function createFileResource($name) 298 | { 299 | if ($this->getResource()->getPattern() && !preg_match($this->getResource()->getPattern(), $name)) { 300 | $this->files[$name] = 'skip'; 301 | } else { 302 | $this->files[$name] = new FileResource($this->getResource()->getResource().'/'.$name); 303 | } 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /src/Lurker/StateChecker/Inotify/FileStateChecker.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class FileStateChecker extends ResourceStateChecker 14 | { 15 | /** 16 | * Initializes checker. 17 | * 18 | * @param CheckerBag $bag 19 | * @param FileResource $resource 20 | * @param int $eventsMask 21 | */ 22 | public function __construct(CheckerBag $bag, FileResource $resource, $eventsMask = FilesystemEvent::ALL) 23 | { 24 | parent::__construct($bag, $resource, $eventsMask); 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function setEvent($mask, $name = '') 31 | { 32 | $this->event = $mask; 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function getChangeset() 39 | { 40 | $changeset = array(); 41 | 42 | $this->handleItself(); 43 | 44 | if ($this->fromInotifyMask($this->event)) { 45 | $changeset[] = 46 | array( 47 | 'resource' => $this->getResource(), 48 | 'event' => $this->fromInotifyMask($this->event) 49 | ); 50 | } 51 | $this->setEvent(false); 52 | 53 | return $changeset; 54 | } 55 | 56 | /** 57 | * Handles event related to itself. 58 | */ 59 | protected function handleItself() 60 | { 61 | if ($this->isMoved($this->event)) { 62 | if ($this->getResource()->exists() && $this->addWatch() === $this->id) { 63 | return; 64 | } 65 | 66 | $this->unwatch($this->id); 67 | } 68 | 69 | if ($this->getResource()->exists()) { 70 | if ($this->isIgnored($this->event) || $this->isMoved($this->event) || !$this->id) { 71 | $this->setEvent($this->id ? IN_MODIFY : IN_CREATE); 72 | $this->watch(); 73 | } 74 | } elseif ($this->id) { 75 | $this->event = IN_DELETE; 76 | $this->getBag()->remove($this); 77 | $this->unwatch($this->id); 78 | $this->id = null; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Lurker/StateChecker/Inotify/NewDirectoryStateChecker.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class NewDirectoryStateChecker extends DirectoryStateChecker 14 | { 15 | /** 16 | * @var bool|null 17 | */ 18 | protected $isNew = true; 19 | 20 | /** 21 | * Initializes checker. 22 | * 23 | * @param CheckerBag $bag 24 | * @param DirectoryResource $resource 25 | * @param int $eventsMask 26 | */ 27 | public function __construct(CheckerBag $bag, DirectoryResource $resource, $eventsMask = FilesystemEvent::ALL) 28 | { 29 | $this->setEvent(IN_CREATE); 30 | parent::__construct($bag, $resource, $eventsMask); 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | protected function createChildCheckers() 37 | { 38 | foreach ($this->getResource()->getFilteredResources() as $resource) { 39 | $basename = basename((string) $resource); 40 | if ($resource instanceof DirectoryResource) { 41 | $this->createNewDirectoryChecker($basename, $resource); 42 | } else { 43 | $this->files[$basename] = $resource; 44 | $this->fileEvents[$basename] = 'new'; 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Lurker/StateChecker/Inotify/ResourceStateChecker.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | abstract class ResourceStateChecker implements StateCheckerInterface 15 | { 16 | /** 17 | * @var int Watch descriptor 18 | */ 19 | protected $id; 20 | 21 | /** 22 | * @var int Inotify event 23 | */ 24 | protected $event; 25 | 26 | /** 27 | * @var CheckerBag 28 | */ 29 | private $bag; 30 | 31 | /** 32 | * @var int 33 | */ 34 | private $eventsMask; 35 | 36 | /** 37 | * @var ResourceInterface 38 | */ 39 | private $resource; 40 | 41 | /** 42 | * Initializes checker. 43 | * 44 | * @param CheckerBag $bag 45 | * @param ResourceInterface $resource 46 | * @param int $eventsMask 47 | */ 48 | public function __construct(CheckerBag $bag, ResourceInterface $resource, $eventsMask = FilesystemEvent::ALL) 49 | { 50 | $this->resource = $resource; 51 | $this->eventsMask = $eventsMask; 52 | $this->bag = $bag; 53 | $this->watch(); 54 | } 55 | 56 | /** 57 | * Returns tracked resource. 58 | * 59 | * @return ResourceInterface 60 | */ 61 | public function getResource() 62 | { 63 | return $this->resource; 64 | } 65 | 66 | /** 67 | * Returns watch descriptor 68 | * 69 | * @return int 70 | */ 71 | public function getId() 72 | { 73 | return $this->id; 74 | } 75 | 76 | /** 77 | * Allows to set event for resource itself or for child resources. 78 | * 79 | * @param int $mask 80 | * @param string $name 81 | */ 82 | abstract public function setEvent($mask, $name = ''); 83 | 84 | /** 85 | * Returns events mask for checker. 86 | * 87 | * @return int 88 | */ 89 | protected function getEventsMask() 90 | { 91 | return $this->eventsMask; 92 | } 93 | 94 | /** 95 | * @return CheckerBag 96 | */ 97 | protected function getBag() 98 | { 99 | return $this->bag; 100 | } 101 | 102 | /** 103 | * Starts to track current resource 104 | */ 105 | protected function watch() 106 | { 107 | if ($this->id) { 108 | $this->bag->remove($this); 109 | } 110 | 111 | $this->id = $this->addWatch(); 112 | $this->bag->add($this); 113 | } 114 | 115 | /** 116 | * Watch resource 117 | * 118 | * @return int 119 | */ 120 | protected function addWatch() 121 | { 122 | return inotify_add_watch($this->getBag()->getInotify(), $this->getResource()->getResource(), $this->getInotifyEventMask()); 123 | } 124 | 125 | /** 126 | * Unwatch resource 127 | * 128 | * @param int $id Watch descriptor 129 | */ 130 | protected function unwatch($id) 131 | { 132 | @inotify_rm_watch($this->bag->getInotify(), $id); 133 | } 134 | 135 | /** 136 | * Transforms inotify event to FilesystemEvent event 137 | * 138 | * @param int $mask 139 | * 140 | * @return bool|int Returns event only if the checker supports it. 141 | */ 142 | protected function fromInotifyMask($mask) 143 | { 144 | $mask &= ~IN_ISDIR; 145 | $event = 0; 146 | switch ($mask) { 147 | case (IN_MODIFY): 148 | case (IN_ATTRIB): 149 | $event = FilesystemEvent::MODIFY; 150 | break; 151 | case (IN_CREATE): 152 | $event = FilesystemEvent::CREATE; 153 | break; 154 | case (IN_DELETE): 155 | case (IN_IGNORED): 156 | $event = FilesystemEvent::DELETE; 157 | } 158 | 159 | return $this->supportsEvent($event) ? $event : false; 160 | } 161 | 162 | /** 163 | * Checks whether checker supports provided resource event. 164 | * 165 | * @param int $event 166 | * 167 | * @return bool 168 | */ 169 | protected function supportsEvent($event) 170 | { 171 | return 0 !== ($this->eventsMask & $event); 172 | } 173 | 174 | /** 175 | * Inotify event mask for inotify_add_watch 176 | * 177 | * @return int 178 | */ 179 | protected function getInotifyEventMask() 180 | { 181 | return IN_MODIFY | IN_ATTRIB | IN_DELETE | IN_CREATE | IN_MOVE | IN_MOVE_SELF; 182 | } 183 | 184 | /** 185 | * Returns true if it is a directory mask 186 | * 187 | * @param int $mask 188 | * 189 | * @return bool 190 | */ 191 | protected function isDir($mask) 192 | { 193 | return 0 !== ($mask & IN_ISDIR); 194 | } 195 | 196 | /** 197 | * Returns true if it is a mask with a IN_DELETE bit active 198 | * 199 | * @param int $mask 200 | * 201 | * @return bool 202 | */ 203 | protected function isDeleted($mask) 204 | { 205 | return 0 !== ($mask & IN_DELETE); 206 | } 207 | 208 | /** 209 | * Returns true if it is a IN_IGNORED mask 210 | * 211 | * @param int $mask 212 | * 213 | * @return bool 214 | */ 215 | protected function isIgnored($mask) 216 | { 217 | return IN_IGNORED === $mask; 218 | } 219 | 220 | /** 221 | * Returns true if it is a IN_MOVE_SELF mask 222 | * 223 | * @param int $mask 224 | * 225 | * @return bool 226 | */ 227 | protected function isMoved($mask) 228 | { 229 | return IN_MOVE_SELF === $mask; 230 | } 231 | 232 | /** 233 | * Returns true if it is a mask with a IN_CREATE bit active 234 | * 235 | * @param int $mask 236 | * 237 | * @return bool 238 | */ 239 | protected function isCreated($mask) 240 | { 241 | return 0 !== ($mask & IN_CREATE); 242 | } 243 | 244 | /** 245 | * Returns true if it is a mask with a IN_MOVED_FROM bit active 246 | * 247 | * @param int $mask 248 | * 249 | * @return bool 250 | */ 251 | protected function isMovedFrom($mask) 252 | { 253 | return 0 !== ($mask & IN_MOVED_FROM); 254 | } 255 | 256 | /** 257 | * Returns true if it is a mask with a IN_MOVED_TO bit active 258 | * 259 | * @param int $mask 260 | * 261 | * @return bool 262 | */ 263 | protected function isMovedTo($mask) 264 | { 265 | return 0 !== ($mask & IN_MOVED_TO); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/Lurker/StateChecker/Inotify/TopDirectoryStateChecker.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class TopDirectoryStateChecker extends DirectoryStateChecker 11 | { 12 | /** 13 | * {@inheritdoc} 14 | */ 15 | protected function handleItself() 16 | { 17 | if ($this->getResource()->exists()) { 18 | if ($this->isMoved($this->event)) { 19 | if ($this->id !== ($id = $this->addWatch())) { 20 | $this->unwatch($this->id); 21 | $this->reindexChildCheckers(); 22 | if ($this->getBag()->has($id)) { 23 | $this->unwatch($id); 24 | } 25 | } 26 | 27 | return; 28 | } 29 | if ($this->isIgnored($this->event) || !$this->id) { 30 | $this->event = $this->id ? null : IN_CREATE; 31 | $this->reindexChildCheckers(); 32 | } 33 | } elseif ($this->id) { 34 | $this->event = IN_DELETE; 35 | $this->unwatch($this->id); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Lurker/StateChecker/NewDirectoryStateChecker.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class NewDirectoryStateChecker extends ResourceStateChecker 14 | { 15 | protected $childs = array(); 16 | 17 | /** 18 | * Initializes checker. 19 | * 20 | * @param DirectoryResource $resource 21 | * @param integer $eventsMask event types bitmask 22 | */ 23 | public function __construct(DirectoryResource $resource, $eventsMask = FilesystemEvent::ALL) 24 | { 25 | parent::__construct($resource, $eventsMask); 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function getChangeset() 32 | { 33 | $changeset = parent::getChangeset(); 34 | 35 | // remove directory modification from changeset 36 | if (isset($changeset[0]) && FilesystemEvent::MODIFY === $changeset[0]['event']) { 37 | $changeset = array(); 38 | } 39 | 40 | // check for changes in already added subfolders/files 41 | foreach ($this->childs as $id => $checker) { 42 | foreach ($checker->getChangeset() as $change) { 43 | if ($this->supportsEvent($change['event'])) { 44 | $changeset[] = $change; 45 | } 46 | } 47 | 48 | // remove checkers for removed resources 49 | if (!$checker->getResource()->exists()) { 50 | unset($this->childs[$id]); 51 | } 52 | } 53 | 54 | // check for new subfolders/files 55 | if ($this->getResource()->exists()) { 56 | foreach ($this->createNewDirectoryChildCheckers($this->getResource()) as $checker) { 57 | $resource = $checker->getResource(); 58 | $resourceId = $resource->getId(); 59 | 60 | if (!isset($this->childs[$resourceId])) { 61 | $this->childs[$resourceId] = $checker; 62 | 63 | if ($this->supportsEvent($event = FilesystemEvent::CREATE)) { 64 | $changeset[] = array( 65 | 'event' => $event, 66 | 'resource' => $resource 67 | ); 68 | } 69 | 70 | // check for new directory changes 71 | if ($checker instanceof NewDirectoryStateChecker) { 72 | foreach ($checker->getChangeset() as $change) { 73 | if ($this->supportsEvent($change['event'])) { 74 | $changeset[] = $change; 75 | } 76 | } 77 | } 78 | } 79 | } 80 | } 81 | 82 | return $changeset; 83 | } 84 | 85 | /** 86 | * Reads files and subdirectories on provided resource path and transform them to resources. 87 | * 88 | * @param DirectoryResource $resource 89 | * 90 | * @return array 91 | */ 92 | protected function createDirectoryChildCheckers(DirectoryResource $resource) 93 | { 94 | $checkers = array(); 95 | foreach ($resource->getFilteredResources() as $resource) { 96 | if ($resource instanceof DirectoryResource) { 97 | $checkers[] = new DirectoryStateChecker($resource, $this->getEventsMask()); 98 | } else { 99 | $checkers[] = new FileStateChecker($resource, $this->getEventsMask()); 100 | } 101 | } 102 | 103 | return $checkers; 104 | } 105 | 106 | /** 107 | * Reads files and subdirectories on provided resource path and transform them to resources. 108 | * 109 | * @param DirectoryResource $resource 110 | * 111 | * @return array 112 | */ 113 | protected function createNewDirectoryChildCheckers(DirectoryResource $resource) 114 | { 115 | $checkers = array(); 116 | foreach ($resource->getFilteredResources() as $resource) { 117 | if ($resource instanceof DirectoryResource) { 118 | $checkers[] = new NewDirectoryStateChecker($resource, $this->getEventsMask()); 119 | } else { 120 | $checkers[] = new FileStateChecker($resource, $this->getEventsMask()); 121 | } 122 | } 123 | 124 | return $checkers; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Lurker/StateChecker/ResourceStateChecker.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | abstract class ResourceStateChecker implements StateCheckerInterface 14 | { 15 | private $resource; 16 | private $timestamp; 17 | private $eventsMask; 18 | private $deleted = false; 19 | 20 | /** 21 | * Initializes checker. 22 | * 23 | * @param ResourceInterface $resource resource 24 | * @param integer $eventsMask event types bitmask 25 | */ 26 | public function __construct(ResourceInterface $resource, $eventsMask = FilesystemEvent::ALL) 27 | { 28 | $this->resource = $resource; 29 | $this->timestamp = $resource->getModificationTime() + 1; 30 | $this->eventsMask = $eventsMask; 31 | $this->deleted = !$resource->exists(); 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function getResource() 38 | { 39 | return $this->resource; 40 | } 41 | 42 | /** 43 | * Returns events mask for checker. 44 | * 45 | * @return integer 46 | */ 47 | public function getEventsMask() 48 | { 49 | return $this->eventsMask; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function getChangeset() 56 | { 57 | $changeset = array(); 58 | 59 | if ($this->deleted) { 60 | if ($this->resource->exists()) { 61 | $this->timestamp = $this->resource->getModificationTime() + 1; 62 | $this->deleted = false; 63 | 64 | if ($this->supportsEvent($event = FilesystemEvent::CREATE)) { 65 | $changeset[] = array( 66 | 'event' => $event, 67 | 'resource' => $this->resource 68 | ); 69 | } 70 | } 71 | } elseif (!$this->resource->exists()) { 72 | $this->deleted = true; 73 | 74 | if ($this->supportsEvent($event = FilesystemEvent::DELETE)) { 75 | $changeset[] = array( 76 | 'event' => $event, 77 | 'resource' => $this->resource 78 | ); 79 | } 80 | } elseif (!$this->resource->isFresh($this->timestamp)) { 81 | $this->timestamp = $this->resource->getModificationTime() + 1; 82 | 83 | if ($this->supportsEvent($event = FilesystemEvent::MODIFY)) { 84 | $changeset[] = array( 85 | 'event' => $event, 86 | 'resource' => $this->resource 87 | ); 88 | } 89 | } 90 | 91 | return $changeset; 92 | } 93 | 94 | /** 95 | * Checks whether checker supports provided resource event. 96 | * 97 | * @param integer $event 98 | * 99 | * @return Boolean 100 | */ 101 | protected function supportsEvent($event) 102 | { 103 | return 0 !== ($this->eventsMask & $event); 104 | } 105 | 106 | /** 107 | * Checks whether resource have been previously deleted. 108 | * 109 | * @return Boolean 110 | */ 111 | protected function isDeleted() 112 | { 113 | return $this->deleted; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Lurker/StateChecker/StateCheckerInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface StateCheckerInterface 13 | { 14 | /** 15 | * Returns tracked resource. 16 | * 17 | * @return ResourceInterface 18 | */ 19 | public function getResource(); 20 | 21 | /** 22 | * Check tracked resource for changes. 23 | * 24 | * @return array 25 | */ 26 | public function getChangeset(); 27 | } 28 | -------------------------------------------------------------------------------- /src/Lurker/Tracker/InotifyTracker.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class InotifyTracker implements TrackerInterface 20 | { 21 | /** 22 | * @var array 23 | */ 24 | protected $checkers = array(); 25 | 26 | /** 27 | * @var CheckerBag 28 | */ 29 | protected $bag; 30 | 31 | /** 32 | * @var resource Inotify resource. 33 | */ 34 | private $inotify; 35 | 36 | /** 37 | * Initializes tracker. Creates inotify resource used to track file and directory changes. 38 | * 39 | * @throws RuntimeException If inotify extension unavailable 40 | */ 41 | public function __construct() 42 | { 43 | if (!function_exists('inotify_init')) { 44 | throw new RuntimeException('You must install inotify to be able to use this tracker.'); 45 | } 46 | 47 | $this->inotify = inotify_init(); 48 | stream_set_blocking($this->inotify, 0); 49 | 50 | $this->bag = new CheckerBag($this->inotify); 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function track(TrackedResource $resource, $eventsMask = FilesystemEvent::ALL) 57 | { 58 | $trackingId = $resource->getTrackingId(); 59 | $checker = $resource->getOriginalResource() instanceof DirectoryResource 60 | ? new TopDirectoryStateChecker($this->bag, $resource->getOriginalResource(), $eventsMask) 61 | : new FileStateChecker($this->bag, $resource->getOriginalResource(), $eventsMask); 62 | 63 | $this->checkers[$trackingId] = array( 64 | 'tracked' => $resource, 65 | 'checker' => $checker 66 | ); 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | * 72 | * @throws RuntimeException If event queue overflowed 73 | */ 74 | public function getEvents() 75 | { 76 | $inotifyEvents = $this->readEvents(); 77 | 78 | $inotifyEvents = is_array($inotifyEvents) ? $inotifyEvents : array(); 79 | 80 | $last = end($inotifyEvents); 81 | if (IN_Q_OVERFLOW === $last['mask']) { 82 | throw new RuntimeException('Event queue overflowed. Either read events more frequently or increase the limit for queues. The limit can be changed in /proc/sys/fs/inotify/max_queued_events'); 83 | } 84 | 85 | foreach ($inotifyEvents as $event) { 86 | foreach ($this->bag->get($event['wd']) as $watched) { 87 | $watched->setEvent($event['mask'], $event['name']); 88 | } 89 | } 90 | 91 | $events = array(); 92 | 93 | foreach ($this->checkers as $meta) { 94 | $tracked = $meta['tracked']; 95 | $watched = $meta['checker']; 96 | foreach ($watched->getChangeset() as $change) { 97 | $events[] = new FilesystemEvent($tracked, $change['resource'], $change['event']); 98 | } 99 | } 100 | 101 | return $events; 102 | } 103 | 104 | /** 105 | * Closes the inotify resource. 106 | */ 107 | public function __destruct() 108 | { 109 | fclose($this->inotify); 110 | } 111 | 112 | /** 113 | * Returns all events happened since last event readout 114 | * 115 | * @return array 116 | */ 117 | protected function readEvents() 118 | { 119 | return inotify_read($this->inotify); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Lurker/Tracker/RecursiveIteratorTracker.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class RecursiveIteratorTracker implements TrackerInterface 17 | { 18 | private $checkers = array(); 19 | 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function track(TrackedResource $resource, $eventsMask = FilesystemEvent::ALL) 24 | { 25 | $trackingId = $resource->getTrackingId(); 26 | $checker = $resource->getOriginalResource() instanceof DirectoryResource 27 | ? new DirectoryStateChecker($resource->getOriginalResource(), $eventsMask) 28 | : new FileStateChecker($resource->getOriginalResource(), $eventsMask); 29 | 30 | $this->checkers[$trackingId] = array( 31 | 'tracked' => $resource, 32 | 'checker' => $checker 33 | ); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function getEvents() 40 | { 41 | $events = array(); 42 | foreach ($this->checkers as $trackingId => $meta) { 43 | $tracked = $meta['tracked']; 44 | $checker = $meta['checker']; 45 | 46 | foreach ($checker->getChangeset() as $change) { 47 | $events[] = new FilesystemEvent($tracked, $change['resource'], $change['event']); 48 | } 49 | } 50 | 51 | return $events; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Lurker/Tracker/TrackerInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface TrackerInterface 14 | { 15 | /** 16 | * Starts to track provided resource for changes. 17 | * 18 | * @param TrackedResource $resource 19 | * @param integer $eventsMask event types bitmask 20 | */ 21 | public function track(TrackedResource $resource, $eventsMask = FilesystemEvent::ALL); 22 | 23 | /** 24 | * Checks tracked resources for change events. 25 | * 26 | * @return array change events array 27 | */ 28 | public function getEvents(); 29 | } 30 | -------------------------------------------------------------------------------- /tests/Lurker/Tests/Event/FilesystemEventTest.php: -------------------------------------------------------------------------------- 1 | assertSame($tracked, $event->getTrackedResource()); 21 | $this->assertSame($resource, $event->getResource()); 22 | $this->assertSame($type, $event->getType()); 23 | } 24 | 25 | public function testIsFileChange() 26 | { 27 | $event = new FilesystemEvent( 28 | $tracked = new TrackedResource(23, new DirectoryResource(__DIR__.'/../')), 29 | $resource = new FileResource(__FILE__), 30 | $type = FilesystemEvent::MODIFY 31 | ); 32 | 33 | $this->assertTrue($event->isFileChange()); 34 | $this->assertFalse($event->isDirectoryChange()); 35 | } 36 | 37 | public function testIsDirectoryChange() 38 | { 39 | $event = new FilesystemEvent( 40 | $tracked = new TrackedResource(23, new DirectoryResource(__DIR__.'/../')), 41 | $resource = new DirectoryResource(__DIR__), 42 | $type = FilesystemEvent::MODIFY 43 | ); 44 | 45 | $this->assertFalse($event->isFileChange()); 46 | $this->assertTrue($event->isDirectoryChange()); 47 | } 48 | 49 | public function testType() 50 | { 51 | $event = new FilesystemEvent( 52 | new TrackedResource(23, new DirectoryResource(__DIR__.'/../')), 53 | new DirectoryResource(__DIR__), 54 | FilesystemEvent::MODIFY 55 | ); 56 | 57 | $this->assertSame(FilesystemEvent::MODIFY, $event->getType()); 58 | $this->assertSame('modify', $event->getTypeString()); 59 | 60 | $event = new FilesystemEvent( 61 | new TrackedResource(23, new DirectoryResource(__DIR__.'/../')), 62 | new DirectoryResource(__DIR__), 63 | FilesystemEvent::DELETE 64 | ); 65 | 66 | $this->assertSame(FilesystemEvent::DELETE, $event->getType()); 67 | $this->assertSame('delete', $event->getTypeString()); 68 | 69 | $event = new FilesystemEvent( 70 | new TrackedResource(23, new DirectoryResource(__DIR__.'/../')), 71 | new DirectoryResource(__DIR__), 72 | FilesystemEvent::CREATE 73 | ); 74 | 75 | $this->assertSame(FilesystemEvent::CREATE, $event->getType()); 76 | $this->assertSame('create', $event->getTypeString()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/Lurker/Tests/ResourceWatcherTest.php: -------------------------------------------------------------------------------- 1 | tracker = $this 19 | ->getMockBuilder('Lurker\\Tracker\\TrackerInterface') 20 | ->getMock(); 21 | 22 | $this->dispatcher = $this 23 | ->getMockBuilder('Symfony\\Component\\EventDispatcher\\EventDispatcherInterface') 24 | ->getMock(); 25 | } 26 | 27 | public function testConstructor() 28 | { 29 | $watcher = new ResourceWatcher($this->tracker, $this->dispatcher); 30 | 31 | $this->assertSame($this->tracker, $watcher->getTracker()); 32 | $this->assertSame($this->dispatcher, $watcher->getEventDispatcher()); 33 | } 34 | 35 | public function testConstructorDefaults() 36 | { 37 | $watcher = new ResourceWatcher; 38 | 39 | if (function_exists('inotify_init')) { 40 | $this->assertInstanceOf( 41 | 'Lurker\\Tracker\\InotifyTracker', 42 | $watcher->getTracker() 43 | ); 44 | } else { 45 | $this->assertInstanceOf( 46 | 'Lurker\\Tracker\\RecursiveIteratorTracker', 47 | $watcher->getTracker() 48 | ); 49 | } 50 | 51 | $this->assertInstanceOf( 52 | 'Symfony\\Component\\EventDispatcher\\EventDispatcher', 53 | $watcher->getEventDispatcher() 54 | ); 55 | } 56 | 57 | public function testTrackResource() 58 | { 59 | $watcher = new ResourceWatcher($this->tracker, $this->dispatcher); 60 | 61 | $resource = $this->getResourceMock(); 62 | $tracked = new TrackedResource('twig.templates', $resource); 63 | 64 | $this->tracker 65 | ->expects($this->once()) 66 | ->method('track') 67 | ->with($tracked, FilesystemEvent::ALL); 68 | 69 | $watcher->track('twig.templates', $resource); 70 | } 71 | 72 | public function testTrackFilepath() 73 | { 74 | $watcher = new ResourceWatcher($this->tracker, $this->dispatcher); 75 | 76 | $resource = __FILE__; 77 | $tracked = new TrackedResource('twig.templates', new FileResource($resource)); 78 | 79 | $this->tracker 80 | ->expects($this->once()) 81 | ->method('track') 82 | ->with($tracked); 83 | 84 | $watcher->track('twig.templates', $resource); 85 | } 86 | 87 | public function testTrackDirpath() 88 | { 89 | $watcher = new ResourceWatcher($this->tracker, $this->dispatcher); 90 | 91 | $resource = __DIR__; 92 | $tracked = new TrackedResource('twig.templates', new DirectoryResource($resource)); 93 | 94 | $this->tracker 95 | ->expects($this->once()) 96 | ->method('track') 97 | ->with($tracked); 98 | 99 | $watcher->track('twig.templates', $resource); 100 | } 101 | 102 | /** 103 | * @expectedException Lurker\Exception\InvalidArgumentException 104 | * @expectedExceptionMessage Second argument to track() should be either file or directory 105 | * resource, but got "unexisting_something" 106 | */ 107 | public function testTrackUnexistingResource() 108 | { 109 | $watcher = new ResourceWatcher($this->tracker, $this->dispatcher); 110 | $watcher->track('twig.templates', 'unexisting_something'); 111 | } 112 | 113 | /** 114 | * @expectedException Lurker\Exception\InvalidArgumentException 115 | * @expectedExceptionMessage "all" is a reserved keyword and can not be used as tracking id 116 | */ 117 | public function testTrackReservedKeyword() 118 | { 119 | $watcher = new ResourceWatcher($this->tracker, $this->dispatcher); 120 | $watcher->track('all', __FILE__); 121 | } 122 | 123 | public function testListenWithCallback() 124 | { 125 | $watcher = new ResourceWatcher($this->tracker, $this->dispatcher); 126 | 127 | $callback = function() {}; 128 | 129 | $this->dispatcher 130 | ->expects($this->once()) 131 | ->method('addListener') 132 | ->with('resource_watcher.twig.templates', $callback); 133 | 134 | $watcher->addListener('twig.templates', $callback); 135 | } 136 | 137 | /** 138 | * @expectedException Lurker\Exception\InvalidArgumentException 139 | * @expectedExceptionMessage Second argument to listen() should be callable, but got string 140 | */ 141 | public function testListenWithWrongCallback() 142 | { 143 | $watcher = new ResourceWatcher($this->tracker, $this->dispatcher); 144 | $watcher->addListener('twig.templates', 'string'); 145 | } 146 | 147 | public function testTrackBy() 148 | { 149 | $callback = function() {}; 150 | 151 | $watcher = $this 152 | ->getMockBuilder('Lurker\\ResourceWatcher') 153 | ->disableOriginalConstructor() 154 | ->setMethods(array('track', 'addListener')) 155 | ->getMock(); 156 | $watcher 157 | ->expects($this->once()) 158 | ->method('track') 159 | ->with(md5(__FILE__.FilesystemEvent::MODIFY), __FILE__, FilesystemEvent::MODIFY); 160 | $watcher 161 | ->expects($this->once()) 162 | ->method('addListener') 163 | ->with(md5(__FILE__.FilesystemEvent::MODIFY), $callback); 164 | 165 | $watcher->trackByListener(__FILE__, $callback, FilesystemEvent::MODIFY); 166 | } 167 | 168 | public function testTracking() 169 | { 170 | $watcher = new ResourceWatcher($this->tracker, $this->dispatcher); 171 | 172 | $this->tracker 173 | ->expects($this->once()) 174 | ->method('getEvents') 175 | ->will($this->returnValue(array( 176 | $e1 = $this->getFSEventMock(), $e2 = $this->getFSEventMock() 177 | ))); 178 | 179 | $e1 180 | ->expects($this->once()) 181 | ->method('getTrackedResource') 182 | ->will($this->returnValue($this->getTrackedResourceMock('trackingId#1'))); 183 | $e2 184 | ->expects($this->once()) 185 | ->method('getTrackedResource') 186 | ->will($this->returnValue($this->getTrackedResourceMock('trackingId#2'))); 187 | 188 | $this->dispatcher 189 | ->expects($this->exactly(4)) 190 | ->method('dispatch') 191 | ->with($this->logicalOr( 192 | 'resource_watcher.trackingId#1', 193 | 'resource_watcher.trackingId#2', 194 | 'resource_watcher.all' 195 | ), $this->logicalOr( 196 | $e1, $e2 197 | )); 198 | 199 | $watcher->start(1,1); 200 | } 201 | 202 | /** 203 | * @group medium 204 | */ 205 | public function testTrackingFunctionally() 206 | { 207 | $file = tempnam(sys_get_temp_dir(), 'sf2_resource_watcher_'); 208 | $event = null; 209 | 210 | $watcher = new ResourceWatcher(); 211 | $watcher->trackByListener($file, function($firedEvent) use(&$event) { 212 | $event = $firedEvent; 213 | }); 214 | 215 | usleep(2000000); 216 | touch($file); 217 | 218 | $watcher->start(1,1); 219 | 220 | $this->assertNotNull($event); 221 | $this->assertSame($file, (string) $event->getResource()); 222 | $this->assertSame(FilesystemEvent::MODIFY, $event->getType()); 223 | 224 | $watcher->stop(); 225 | 226 | unlink($file); 227 | 228 | $watcher->start(1,1); 229 | 230 | $this->assertNotNull($event); 231 | $this->assertSame($file, (string) $event->getResource()); 232 | $this->assertSame(FilesystemEvent::DELETE, $event->getType()); 233 | } 234 | 235 | protected function getResourceMock() 236 | { 237 | $resource = $this->getMock('Lurker\\Resource\\ResourceInterface'); 238 | 239 | $resource 240 | ->expects($this->any()) 241 | ->method('exists') 242 | ->will($this->returnValue(true)); 243 | 244 | return $resource; 245 | } 246 | 247 | protected function getFSEventMock() 248 | { 249 | return $this 250 | ->getMockBuilder('Lurker\\Event\\FilesystemEvent') 251 | ->disableOriginalConstructor() 252 | ->getMock(); 253 | } 254 | 255 | public function getTrackedResourceMock($trackingId = null) 256 | { 257 | $resource = $this 258 | ->getMockBuilder('Lurker\\Resource\\TrackedResource') 259 | ->disableOriginalConstructor() 260 | ->getMock(); 261 | 262 | if (null !== $trackingId) { 263 | $resource 264 | ->expects($this->any()) 265 | ->method('getTrackingId') 266 | ->will($this->returnValue($trackingId)); 267 | } 268 | 269 | return $resource; 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /tests/Lurker/Tests/StateChecker/DirectoryStateCheckerTest.php: -------------------------------------------------------------------------------- 1 | createDirectoryResourceMock(); 16 | $resource 17 | ->expects($this->any()) 18 | ->method('getFilteredResources') 19 | ->will($this->returnValue(array( 20 | $foo = $this->createDirectoryResourceMock() 21 | ))); 22 | 23 | $resource 24 | ->expects($this->any()) 25 | ->method('getModificationTime') 26 | ->will($this->returnValue(11)); 27 | 28 | $foo 29 | ->expects($this->any()) 30 | ->method('getFilteredResources') 31 | ->will($this->returnValue(array( 32 | $foobar = $this->createFileResourceMock() 33 | ))); 34 | $foo 35 | ->expects($this->any()) 36 | ->method('getModificationTime') 37 | ->will($this->returnValue(22)); 38 | 39 | $foobar 40 | ->expects($this->any()) 41 | ->method('getModificationTime') 42 | ->will($this->returnValue(33)); 43 | 44 | $checker = new DirectoryStateChecker($resource); 45 | 46 | $this->touchResource($resource, true, true); 47 | $this->touchResource($foo, true, true); 48 | $this->touchResource($foobar, true, false); 49 | 50 | $this->assertEquals(array( 51 | array('event' => FilesystemEvent::MODIFY, 'resource' => $foobar) 52 | ), $checker->getChangeset()); 53 | } 54 | 55 | public function testDeepFileDeleted() 56 | { 57 | $resource = $this->createDirectoryResourceMock(); 58 | $resource 59 | ->expects($this->any()) 60 | ->method('getFilteredResources') 61 | ->will($this->returnValue(array( 62 | $foo = $this->createDirectoryResourceMock() 63 | ))); 64 | $resource 65 | ->expects($this->any()) 66 | ->method('getModificationTime') 67 | ->will($this->returnValue(11)); 68 | $foo 69 | ->expects($this->any()) 70 | ->method('getFilteredResources') 71 | ->will($this->onConsecutiveCalls( 72 | array($foobar = $this->createFileResourceMock(array(true, false))), 73 | array() 74 | )); 75 | $foo 76 | ->expects($this->any()) 77 | ->method('getModificationTime') 78 | ->will($this->returnValue(22)); 79 | $foobar 80 | ->expects($this->any()) 81 | ->method('getModificationTime') 82 | ->will($this->returnValue(33)); 83 | 84 | $checker = new DirectoryStateChecker($resource); 85 | 86 | $this->touchResource($resource, true, true); 87 | $this->touchResource($foo, true, true); 88 | 89 | $this->assertEquals(array( 90 | array('event' => FilesystemEvent::DELETE, 'resource' => $foobar) 91 | ), $checker->getChangeset()); 92 | } 93 | 94 | public function testDeepFileCreated() 95 | { 96 | $resource = $this->createDirectoryResourceMock(); 97 | $resource 98 | ->expects($this->any()) 99 | ->method('getFilteredResources') 100 | ->will($this->returnValue(array( 101 | $foo = $this->createDirectoryResourceMock() 102 | ))); 103 | $resource 104 | ->expects($this->any()) 105 | ->method('getModificationTime') 106 | ->will($this->returnValue(11)); 107 | $foo 108 | ->expects($this->any()) 109 | ->method('getFilteredResources') 110 | ->will($this->onConsecutiveCalls( 111 | array(), 112 | array($foobar = $this->createFileResourceMock()) 113 | )); 114 | $foo 115 | ->expects($this->any()) 116 | ->method('getModificationTime') 117 | ->will($this->returnValue(22)); 118 | $foobar 119 | ->expects($this->any()) 120 | ->method('getModificationTime') 121 | ->will($this->returnValue(33)); 122 | 123 | $checker = new DirectoryStateChecker($resource); 124 | 125 | $this->touchResource($resource, true, true); 126 | $this->touchResource($foo, true, true); 127 | 128 | $this->assertEquals(array( 129 | array('event' => FilesystemEvent::CREATE, 'resource' => $foobar) 130 | ), $checker->getChangeset()); 131 | } 132 | 133 | protected function touchResource(ResourceInterface $resource, $exists = true, $fresh = true) 134 | { 135 | if ($exists) { 136 | $resource 137 | ->expects($this->any()) 138 | ->method('isFresh') 139 | ->will($this->returnValue($fresh)); 140 | } else { 141 | $resource 142 | ->expects($this->any()) 143 | ->method('exists') 144 | ->will($this->returnValue(false)); 145 | } 146 | } 147 | 148 | protected function createDirectoryResourceMock($exists = true) 149 | { 150 | $resource = $this->getMockBuilder('Lurker\Resource\DirectoryResource') 151 | ->disableOriginalConstructor() 152 | ->getMock(); 153 | 154 | $this->setResourceExists($resource, $exists); 155 | 156 | return $resource; 157 | } 158 | 159 | protected function createFileResourceMock($exists = true) 160 | { 161 | $resource = $this->getMockBuilder('Lurker\Resource\FileResource') 162 | ->disableOriginalConstructor() 163 | ->getMock(); 164 | 165 | $this->setResourceExists($resource, $exists); 166 | 167 | return $resource; 168 | } 169 | 170 | protected function setResourceExists($resource, $exists) 171 | { 172 | if (is_array($exists)) { 173 | $resource 174 | ->expects($this->any()) 175 | ->method('exists') 176 | ->will(call_user_func_array(array($this, 'onConsecutiveCalls'), $exists)); 177 | } else { 178 | $resource 179 | ->expects($this->any()) 180 | ->method('exists') 181 | ->will($this->returnValue($exists)); 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /tests/Lurker/Tests/StateChecker/FileStateCheckerTest.php: -------------------------------------------------------------------------------- 1 | createResource(); 13 | $checker = $this->createChecker($resource); 14 | 15 | $this->assertSame($resource, $checker->getResource()); 16 | } 17 | 18 | public function testNoChanges() 19 | { 20 | $resource = $this->createResource(true); 21 | $checker = $this->createChecker($resource); 22 | 23 | $resource 24 | ->expects($this->once()) 25 | ->method('isFresh') 26 | ->with(12) 27 | ->will($this->returnValue(true)); 28 | 29 | $this->assertEquals(array(), $checker->getChangeset()); 30 | } 31 | 32 | public function testDeleted() 33 | { 34 | $resource = $this->createResource(null); 35 | $resource 36 | ->expects($this->any()) 37 | ->method('exists') 38 | ->will($this->onConsecutiveCalls(true, false)); 39 | 40 | $checker = $this->createChecker($resource); 41 | 42 | $this->assertEquals( 43 | array(array('event' => FilesystemEvent::DELETE, 'resource' => $resource)), 44 | $checker->getChangeset() 45 | ); 46 | } 47 | 48 | public function testModified() 49 | { 50 | $resource = $this->createResource(true); 51 | $checker = $this->createChecker($resource); 52 | 53 | $resource 54 | ->expects($this->once()) 55 | ->method('isFresh') 56 | ->with(12) 57 | ->will($this->returnValue(false)); 58 | 59 | $this->assertEquals( 60 | array(array('event' => FilesystemEvent::MODIFY, 'resource' => $resource)), 61 | $checker->getChangeset() 62 | ); 63 | } 64 | 65 | public function testConsecutiveChecks() 66 | { 67 | $resource = $this->createResource(null); 68 | $resource 69 | ->expects($this->any()) 70 | ->method('exists') 71 | ->will($this->onConsecutiveCalls(true, true, false)); 72 | $checker = $this->createChecker($resource); 73 | 74 | $resource 75 | ->expects($this->once()) 76 | ->method('isFresh') 77 | ->with(12) 78 | ->will($this->returnValue(false)); 79 | 80 | $this->assertEquals( 81 | array(array('event' => FilesystemEvent::MODIFY, 'resource' => $resource)), 82 | $checker->getChangeset() 83 | ); 84 | 85 | $this->assertEquals( 86 | array(array('event' => FilesystemEvent::DELETE, 'resource' => $resource)), 87 | $checker->getChangeset() 88 | ); 89 | 90 | $this->assertEquals(array(), $checker->getChangeset()); 91 | } 92 | 93 | protected function createResource($exists = true) 94 | { 95 | $resource = $this 96 | ->getMockBuilder('Lurker\Resource\FileResource') 97 | ->disableOriginalConstructor() 98 | ->getMock(); 99 | 100 | $resource 101 | ->expects($this->any()) 102 | ->method('getModificationTime') 103 | ->will($this->returnValue(11)); 104 | 105 | if (null !== $exists) { 106 | $resource 107 | ->expects($this->any()) 108 | ->method('exists') 109 | ->will($this->returnValue($exists)); 110 | } 111 | 112 | return $resource; 113 | } 114 | 115 | protected function createChecker($resource) 116 | { 117 | return new FileStateChecker($resource); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tests/Lurker/Tests/StateChecker/Inotify/DirectoryStateCheckerTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Inotify is required for this test'); 20 | } 21 | 22 | $this->bag = new CheckerBag('whatever'); 23 | $this->resource = $this->getResource(); 24 | } 25 | 26 | public function testResourceAddedToBag() 27 | { 28 | $this->setAddWatchReturns(1); 29 | $checker = $this->getChecker(); 30 | 31 | $this->assertCount(1, $this->bag->get(1)); 32 | $this->assertContains($checker, $this->bag->get(1)); 33 | } 34 | 35 | public function testResourceDeleted() 36 | { 37 | $this->setAddWatchReturns(1); 38 | $this->markResourceNonExistent(); 39 | $checker = $this->getChecker($this->resource); 40 | $checker->setEvent(IN_DELETE); 41 | 42 | $events = $checker->getChangeset(); 43 | 44 | $this->assertHasEvent($this->resource, FilesystemEvent::DELETE, $events); 45 | $this->assertCount(0, $this->bag->get(1)); 46 | $this->assertNull($checker->getId()); 47 | } 48 | 49 | protected function setAddWatchReturns($id) 50 | { 51 | DirectoryStateCheckerForTest::setAddWatchReturns($id); 52 | } 53 | 54 | protected function getChecker() 55 | { 56 | return new DirectoryStateCheckerForTest($this->bag, $this->resource); 57 | } 58 | 59 | protected function assertHasEvent(ResourceInterface $resource, $event, $events) 60 | { 61 | $this->assertContains(array('resource' => $resource, 'event' => $event), $events, sprintf('Cannot find the expected event for the received resource.')); 62 | } 63 | 64 | protected function getResource() 65 | { 66 | $resource = $this 67 | ->getMockBuilder('Lurker\Resource\DirectoryResource') 68 | ->disableOriginalConstructor() 69 | ->getMock(); 70 | $resource 71 | ->expects($this->any()) 72 | ->method('exists') 73 | ->will($this->returnCallback(array($this, 'isResourceExists'))); 74 | $resource 75 | ->expects($this->any()) 76 | ->method('getFilteredResources') 77 | ->will($this->returnValue(array())); 78 | 79 | return $resource; 80 | } 81 | 82 | protected function markResourceExistent() 83 | { 84 | $this->exists = true; 85 | } 86 | 87 | protected function markResourceNonExistent() 88 | { 89 | $this->exists = false; 90 | } 91 | 92 | public function isResourceExists() 93 | { 94 | return $this->exists; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/Lurker/Tests/StateChecker/Inotify/FileStateCheckerTest.php: -------------------------------------------------------------------------------- 1 | setAddWatchReturns(1); 13 | $checker = $this->getChecker(); 14 | $checker->setEvent(IN_MOVE_SELF); 15 | 16 | $this->setAddWatchReturns(2); 17 | $events = $checker->getChangeset(); 18 | 19 | $this->assertHasEvent($this->resource, FilesystemEvent::MODIFY, $events); 20 | $this->assertCount(0, $this->bag->get(1)); 21 | $this->assertCount(1, $this->bag->get(2)); 22 | $this->assertContains($checker, $this->bag->get(2)); 23 | } 24 | 25 | protected function setAddWatchReturns($id) 26 | { 27 | FileStateCheckerForTest::setAddWatchReturns($id); 28 | } 29 | 30 | protected function getChecker() 31 | { 32 | return new FileStateCheckerForTest($this->bag, $this->resource); 33 | } 34 | 35 | protected function getResource() 36 | { 37 | $resource = $this 38 | ->getMockBuilder('Lurker\Resource\FileResource') 39 | ->disableOriginalConstructor() 40 | ->getMock(); 41 | $resource 42 | ->expects($this->any()) 43 | ->method('exists') 44 | ->will($this->returnCallback(array($this, 'isResourceExists'))); 45 | 46 | return $resource; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Lurker/Tests/StateChecker/Inotify/Fixtures/DirectoryStateCheckerForTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Inotify is required for this test'); 19 | } 20 | 21 | $this->bag = new CheckerBag('whatever'); 22 | $this->resource = $this->getResource(); 23 | } 24 | 25 | public function testResourceAddedToBag() 26 | { 27 | $this->setAddWatchReturns(1); 28 | $checker = $this->getChecker(); 29 | 30 | $this->assertCount(1, $this->bag->get(1)); 31 | $this->assertContains($checker, $this->bag->get(1)); 32 | } 33 | 34 | public function testResourceDeleted() 35 | { 36 | $this->setAddWatchReturns(1); 37 | $this->markResourceNonExistent(); 38 | $checker = $this->getChecker(); 39 | $checker->setEvent(IN_IGNORED); 40 | 41 | $events = $checker->getChangeset(); 42 | 43 | $this->assertHasEvent($this->resource, FilesystemEvent::DELETE, $events); 44 | $this->assertCount(0, $this->bag->get(1)); 45 | $this->assertNull($checker->getId()); 46 | } 47 | 48 | public function testResourceCreated() 49 | { 50 | $this->setAddWatchReturns(1); 51 | $this->markResourceNonExistent(); 52 | $checker = $this->getChecker(); 53 | $checker->setEvent(IN_IGNORED); 54 | 55 | $checker->getChangeset(); 56 | 57 | $this->setAddWatchReturns(2); 58 | $this->markResourceExistent(); 59 | $events = $checker->getChangeset(); 60 | 61 | $this->assertHasEvent($this->resource, FilesystemEvent::CREATE, $events); 62 | $this->assertCount(1, $this->bag->get(2)); 63 | $this->assertContains($checker, $this->bag->get(2)); 64 | } 65 | 66 | public function testResourceMoved() 67 | { 68 | $this->setAddWatchReturns(1); 69 | $this->markResourceNonExistent(); 70 | $checker = $this->getChecker(); 71 | $checker->setEvent(IN_MOVE_SELF); 72 | 73 | $events = $checker->getChangeset(); 74 | 75 | $this->assertHasEvent($this->resource, FilesystemEvent::DELETE, $events); 76 | $this->assertCount(0, $this->bag->get(1)); 77 | $this->assertNull($checker->getId()); 78 | 79 | $this->setAddWatchReturns(2); 80 | $this->markResourceExistent(); 81 | $events = $checker->getChangeset(); 82 | 83 | $this->assertHasEvent($this->resource, FilesystemEvent::CREATE, $events); 84 | $this->assertCount(1, $this->bag->get(2)); 85 | $this->assertContains($checker, $this->bag->get(2)); 86 | } 87 | 88 | public function testResourceMovedAndReturnedSameWatchId() 89 | { 90 | $this->setAddWatchReturns(1); 91 | $checker = $this->getChecker(); 92 | $checker->setEvent(IN_MOVE_SELF); 93 | 94 | $events = $checker->getChangeset(); 95 | $this->assertEmpty($events); 96 | } 97 | 98 | public function isResourceExists() 99 | { 100 | return $this->exists; 101 | } 102 | 103 | protected function assertHasEvent(ResourceInterface $resource, $event, $events) 104 | { 105 | $this->assertContains(array('resource' => $resource, 'event' => $event), $events, sprintf('Cannot find the expected event for the received resource')); 106 | } 107 | 108 | protected function markResourceExistent() 109 | { 110 | $this->exists = true; 111 | } 112 | 113 | protected function markResourceNonExistent() 114 | { 115 | $this->exists = false; 116 | } 117 | 118 | abstract protected function setAddWatchReturns($id); 119 | abstract protected function getChecker(); 120 | abstract protected function getResource(); 121 | } 122 | -------------------------------------------------------------------------------- /tests/Lurker/Tests/StateChecker/Inotify/TopDirectoryStateCheckerTest.php: -------------------------------------------------------------------------------- 1 | setAddWatchReturns(1); 12 | $checker = $this->getChecker(); 13 | $checker->setEvent(IN_MOVE_SELF); 14 | 15 | $this->setAddWatchReturns(2); 16 | $events = $checker->getChangeset(); 17 | 18 | $this->assertCount(0, $events); 19 | $this->assertCount(0, $this->bag->get(1)); 20 | $this->assertCount(1, $this->bag->get(2)); 21 | $this->assertContains($checker, $this->bag->get(2)); 22 | } 23 | 24 | protected function setAddWatchReturns($id) 25 | { 26 | TopDirectoryStateCheckerForTest::setAddWatchReturns($id); 27 | } 28 | 29 | protected function getChecker() 30 | { 31 | return new TopDirectoryStateCheckerForTest($this->bag, $this->resource); 32 | } 33 | 34 | protected function getResource() 35 | { 36 | $resource = $this 37 | ->getMockBuilder('Lurker\Resource\DirectoryResource') 38 | ->disableOriginalConstructor() 39 | ->getMock(); 40 | $resource 41 | ->expects($this->any()) 42 | ->method('exists') 43 | ->will($this->returnCallback(array($this, 'isResourceExists'))); 44 | $resource 45 | ->expects($this->any()) 46 | ->method('getFilteredResources') 47 | ->will($this->returnValue(array())); 48 | 49 | return $resource; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Lurker/Tests/Tracker/InotifyTrackerTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Inotify is required for this test'); 17 | } 18 | 19 | parent::setUp(); 20 | } 21 | 22 | /** 23 | * @return TrackerInterface 24 | */ 25 | protected function getTracker() 26 | { 27 | return new InotifyTracker(); 28 | } 29 | 30 | protected function getMinimumInterval() 31 | { 32 | return 0; 33 | } 34 | 35 | /** 36 | * @expectedException Lurker\Exception\RuntimeException 37 | */ 38 | public function testEventOverflow() 39 | { 40 | $tracker = $this->getMockBuilder('Lurker\Tracker\InotifyTracker') 41 | ->disableOriginalConstructor() 42 | ->setMethods(array('readEvents', '__destruct')) 43 | ->getMock(); 44 | $tracker->expects($this->any()) 45 | ->method('readEvents') 46 | ->will($this->returnValue(array(array('mask' => IN_Q_OVERFLOW)))); 47 | 48 | $tracker->getEvents(); 49 | } 50 | 51 | public function testFileDeletionCreationTriggersModifyEvent() 52 | { 53 | $tracker = $this->getTracker(); 54 | 55 | touch($foo = $this->tmpDir.'/foo'); 56 | mkdir($dir = $this->tmpDir.'/dir'); 57 | mkdir($subdir = $dir.'/subdir'); 58 | touch($file = $dir.'/file'); 59 | 60 | $tracker->track(new TrackedResource('foo', $resource = new FileResource($foo))); 61 | $tracker->track(new TrackedResource('dir', $resource = new DirectoryResource($dir))); 62 | 63 | unlink($foo); 64 | touch($foo); 65 | unlink($file); 66 | touch($file); 67 | 68 | $events = $tracker->getEvents(); 69 | $this->assertCount(2, $events); 70 | 71 | $this->assertHasResourceEvent($file, FilesystemEvent::MODIFY, $events); 72 | $this->assertHasResourceEvent($foo, FilesystemEvent::MODIFY, $events); 73 | 74 | rmdir($subdir); 75 | mkdir($subdir); 76 | 77 | $events = $tracker->getEvents(); 78 | $this->assertCount(0, $events); 79 | } 80 | 81 | public function testNewResourceDeletionCreationTriggersNoEvents() 82 | { 83 | $tracker = $this->getTracker(); 84 | 85 | mkdir($dir = $this->tmpDir.'/dir'); 86 | 87 | $tracker->track(new TrackedResource('dir', $resource = new DirectoryResource($dir))); 88 | 89 | mkdir($subdir = $dir.'/subdir'); 90 | touch($subfile = $subdir.'/subfile'); 91 | unlink($subfile); 92 | rmdir($subdir); 93 | touch($file = $dir.'/file'); 94 | unlink($file); 95 | 96 | $events = $tracker->getEvents(); 97 | $this->assertCount(0, $events); 98 | } 99 | 100 | public function testDeletedWatchedResourceStillReturnsEvents() 101 | { 102 | $tracker = $this->getTracker(); 103 | 104 | touch($foo = $this->tmpDir.'/foo'); 105 | mkdir($dir = $this->tmpDir.'/dir'); 106 | 107 | $tracker->track(new TrackedResource('foo', $resource = new FileResource($foo))); 108 | $tracker->track(new TrackedResource('dir', $resource = new DirectoryResource($dir))); 109 | 110 | rmdir($dir); 111 | unlink($foo); 112 | 113 | $tracker->getEvents(); 114 | 115 | touch($foo); 116 | mkdir($dir); 117 | 118 | $events = $tracker->getEvents(); 119 | $this->assertCount(2, $events); 120 | 121 | $this->assertHasResourceEvent($foo, FilesystemEvent::CREATE, $events); 122 | $this->assertHasResourceEvent($dir, FilesystemEvent::CREATE, $events); 123 | 124 | touch($foo); 125 | touch($file = $dir.'/file'); 126 | 127 | $events = $tracker->getEvents(); 128 | $this->assertCount(2, $events); 129 | 130 | $this->assertHasResourceEvent($file, FilesystemEvent::CREATE, $events); 131 | $this->assertHasResourceEvent($foo, FilesystemEvent::MODIFY, $events); 132 | } 133 | 134 | public function testSymlink() 135 | { 136 | $tracker = $this->getTracker(); 137 | 138 | mkdir($dir = $this->tmpDir.'/dir'); 139 | touch($file = $dir.'/file'); 140 | @symlink($file, $link = $file.'_link'); 141 | 142 | if (!is_link($link)) { 143 | $this->markTestSkipped('Working "symlink" function is required for this test.'); 144 | } 145 | 146 | $tracker->track(new TrackedResource('dir', $resource = new DirectoryResource($dir))); 147 | 148 | unlink($link); 149 | 150 | $events = $tracker->getEvents(); 151 | $this->assertCount(0, $events); 152 | } 153 | 154 | public function testTrackSameResourceTwice() 155 | { 156 | $tracker = $this->getTracker(); 157 | 158 | mkdir($dir = $this->tmpDir.'/dir'); 159 | mkdir($subdir = $dir.'/subdir'); 160 | touch($foo = $this->tmpDir.'/foo'); 161 | 162 | $tracker->track(new TrackedResource('dir1', $resource = new DirectoryResource($dir))); 163 | $tracker->track(new TrackedResource('dir2', $resource = new DirectoryResource($dir))); 164 | $tracker->track(new TrackedResource('foo1', $resource = new FileResource($foo))); 165 | $tracker->track(new TrackedResource('foo2', $resource = new FileResource($foo))); 166 | 167 | rename($dir, $dir1 = $dir.'_new'); 168 | rename($foo, $foo1 = $foo.'_new'); 169 | 170 | $events = $tracker->getEvents(); 171 | $this->assertCount(6, $events); 172 | 173 | rename($dir1, $dir); 174 | rename($foo1, $foo); 175 | 176 | $events = $tracker->getEvents(); 177 | $this->assertCount(6, $events); 178 | 179 | touch($file = $dir.'/file'); 180 | file_put_contents($foo, 'content'); 181 | 182 | $events = $tracker->getEvents(); 183 | $this->assertCount(4, $events); 184 | $this->assertHasResourceEvent($file, FilesystemEvent::CREATE, $events); 185 | $this->assertHasResourceEvent($foo, FilesystemEvent::MODIFY, $events); 186 | } 187 | 188 | public function testIgnoreAttribEventForDirectories() 189 | { 190 | $tracker = $this->getTracker(); 191 | 192 | mkdir($dir = $this->tmpDir.'/dir'); 193 | 194 | $tracker->track(new TrackedResource('dir', $resource = new DirectoryResource($dir))); 195 | 196 | touch($dir); 197 | 198 | $events = $tracker->getEvents(); 199 | $this->assertCount(0, $events); 200 | } 201 | 202 | public function testMoveResource() 203 | { 204 | $tracker = $this->getTracker(); 205 | 206 | mkdir($dir = $this->tmpDir.'/dir'); 207 | touch($file = $dir.'/file'); 208 | mkdir($subdir = $dir.'/subdir'); 209 | touch($subfile = $subdir.'/subfile'); 210 | 211 | $tracker->track(new TrackedResource('dir', $resource = new DirectoryResource($dir))); 212 | 213 | rename($file, $file_new = $file.'_new'); 214 | rename($subdir, $subdir_new = $subdir.'_new'); 215 | 216 | $events = $tracker->getEvents(); 217 | $this->assertCount(6, $events); 218 | $this->assertHasResourceEvent($file_new, FilesystemEvent::CREATE, $events); 219 | $this->assertHasResourceEvent($file, FilesystemEvent::DELETE, $events); 220 | $this->assertHasResourceEvent($subdir_new, FilesystemEvent::CREATE, $events); 221 | $this->assertHasResourceEvent($subdir, FilesystemEvent::DELETE, $events); 222 | $this->assertHasResourceEvent($subdir_new.'/subfile', FilesystemEvent::CREATE, $events); 223 | $this->assertHasResourceEvent($subfile, FilesystemEvent::DELETE, $events); 224 | } 225 | 226 | public function testMoveParentDirectoryOfWatchedResource() 227 | { 228 | $tracker = $this->getTracker(); 229 | 230 | mkdir($parent_dir = $this->tmpDir.'/parent_dir'); 231 | mkdir($dir = $parent_dir.'/dir'); 232 | touch($file = $dir.'/file'); 233 | touch($foo = $parent_dir.'/foo'); 234 | 235 | $tracker->track(new TrackedResource('dir', $resource = new DirectoryResource($dir))); 236 | $tracker->track(new TrackedResource('foo', $resource = new FileResource($foo))); 237 | 238 | rename($parent_dir, $parent_dir.'_new'); 239 | 240 | $events = $tracker->getEvents(); 241 | $this->assertCount(3, $events); 242 | $this->assertHasResourceEvent($dir, FilesystemEvent::DELETE, $events); 243 | $this->assertHasResourceEvent($foo, FilesystemEvent::DELETE, $events); 244 | $this->assertHasResourceEvent($file, FilesystemEvent::DELETE, $events); 245 | 246 | rename($parent_dir.'_new', $parent_dir); 247 | 248 | $events = $tracker->getEvents(); 249 | $this->assertCount(3, $events); 250 | $this->assertHasResourceEvent($dir, FilesystemEvent::CREATE, $events); 251 | $this->assertHasResourceEvent($foo, FilesystemEvent::CREATE, $events); 252 | $this->assertHasResourceEvent($file, FilesystemEvent::CREATE, $events); 253 | 254 | touch($file); 255 | touch($foo); 256 | 257 | $events = $tracker->getEvents(); 258 | $this->assertCount(2, $events); 259 | $this->assertHasResourceEvent($file, FilesystemEvent::MODIFY, $events); 260 | $this->assertHasResourceEvent($foo, FilesystemEvent::MODIFY, $events); 261 | } 262 | 263 | public function testMoveResourceBackAndForth() 264 | { 265 | $tracker = $this->getTracker(); 266 | 267 | mkdir($dir = $this->tmpDir.'/dir'); 268 | touch($file = $dir.'/file'); 269 | mkdir($subdir = $dir.'/subdir'); 270 | touch($subfile = $subdir.'/subfile'); 271 | touch($foo = $this->tmpDir.'/foo'); 272 | 273 | $tracker->track(new TrackedResource('dir', $resource = new DirectoryResource($dir))); 274 | $tracker->track(new TrackedResource('foo', $resource = new FileResource($foo))); 275 | 276 | // top resources 277 | rename($dir, $dir.'_new'); 278 | rename($dir.'_new', $dir); 279 | rename($foo, $foo.'_new'); 280 | rename($foo.'_new', $foo); 281 | 282 | $events = $tracker->getEvents(); 283 | $this->assertCount(0, $events); 284 | 285 | // sub resources 286 | rename($subdir, $subdir.'_new'); 287 | rename($subdir.'_new', $subdir.'_new1'); 288 | touch($subdir.'_new1'); 289 | rename($subdir.'_new1', $subdir); 290 | rename($file, $file_new = $file.'_new'); 291 | rename($file_new, $file); 292 | 293 | $events = $tracker->getEvents(); 294 | $this->assertCount(0, $events); 295 | 296 | rename($file, $file.'_new'); 297 | touch($file_new); 298 | rename($file_new, $file); 299 | 300 | $events = $tracker->getEvents(); 301 | $this->assertCount(1, $events); 302 | $this->assertHasResourceEvent($file, FilesystemEvent::MODIFY, $events); 303 | } 304 | 305 | public function testMoveAndCreateNewResourceWithIdenticalName() 306 | { 307 | $tracker = $this->getTracker(); 308 | 309 | mkdir($dir = $this->tmpDir.'/dir'); 310 | touch($file = $dir.'/file'); 311 | mkdir($subdir = $dir.'/subdir'); 312 | touch($subfile = $subdir.'/subfile'); 313 | 314 | $tracker->track(new TrackedResource('dir', $resource = new DirectoryResource($dir))); 315 | 316 | rename($dir, $dir.'_new'); 317 | mkdir($dir); 318 | 319 | $events = $tracker->getEvents(); 320 | $this->assertCount(3, $events); 321 | 322 | $this->assertHasResourceEvent($file, FilesystemEvent::DELETE, $events); 323 | $this->assertHasResourceEvent($subdir, FilesystemEvent::DELETE, $events); 324 | $this->assertHasResourceEvent($subfile, FilesystemEvent::DELETE, $events); 325 | } 326 | 327 | public function testMoveAndCreateNewDifferentResourceWithIdenticalName() 328 | { 329 | $tracker = $this->getTracker(); 330 | 331 | mkdir($dir = $this->tmpDir.'/dir'); 332 | touch($file = $dir.'/file'); 333 | 334 | $tracker->track(new TrackedResource('dir', $resource = new DirectoryResource($dir))); 335 | 336 | rename($dir, $dir.'_new'); 337 | touch($dir); 338 | 339 | $events = $tracker->getEvents(); 340 | $this->assertCount(2, $events); 341 | 342 | $this->assertHasResourceEvent($dir, FilesystemEvent::DELETE, $events); 343 | $this->assertHasResourceEvent($file, FilesystemEvent::DELETE, $events); 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /tests/Lurker/Tests/Tracker/RecursiveIteratorTrackerTest.php: -------------------------------------------------------------------------------- 1 | tmpDir = sys_get_temp_dir().'/sf2_resource_watcher_tests'; 18 | if (is_dir($this->tmpDir)) { 19 | $this->cleanDir($this->tmpDir); 20 | } 21 | 22 | mkdir($this->tmpDir); 23 | 24 | $this->tmpDir = realpath($this->tmpDir); 25 | } 26 | 27 | public function tearDown() 28 | { 29 | $this->cleanDir($this->tmpDir); 30 | } 31 | 32 | /** 33 | * @expectedException Lurker\Exception\InvalidArgumentException 34 | */ 35 | public function testDoesNotTrackMissingFiles() 36 | { 37 | $tracker = $this->getTracker(); 38 | 39 | $tracker->track(new TrackedResource('missing', new FileResource(__DIR__.'/missingfile'))); 40 | } 41 | 42 | /** 43 | * @expectedException Lurker\Exception\InvalidArgumentException 44 | */ 45 | public function testDoesNotTrackMissingDirectories() 46 | { 47 | $tracker = $this->getTracker(); 48 | 49 | $tracker->track(new TrackedResource('missing', new DirectoryResource(__DIR__.'/missingdir'))); 50 | } 51 | 52 | public function testDeleteResourceAndCreateDifferentOne() 53 | { 54 | $tracker = $this->getTracker(); 55 | 56 | mkdir($dir = $this->tmpDir.'/dir'); 57 | touch($file = $dir.'/file'); 58 | 59 | $tracker->track(new TrackedResource('dir', $resource = new DirectoryResource($dir))); 60 | 61 | unlink($file); 62 | mkdir($file); 63 | $this->sleep(); 64 | 65 | $events = $tracker->getEvents(); 66 | $this->assertCount(2, $events); 67 | 68 | $this->assertHasResourceEvent($file, FilesystemEvent::DELETE, $events); 69 | $this->assertHasResourceEvent($file, FilesystemEvent::CREATE, $events); 70 | } 71 | 72 | public function testTrackSimpleFileChanges() 73 | { 74 | $tracker = $this->getTracker(); 75 | 76 | touch($file = $this->tmpDir.'/foo'); 77 | 78 | $tracker->track(new TrackedResource('foo', $resource = new FileResource($file))); 79 | 80 | $this->sleep(); 81 | touch($file); 82 | 83 | $events = $tracker->getEvents(); 84 | $this->assertCount(1, $events); 85 | $this->assertHasResourceEvent($file, FilesystemEvent::MODIFY, $events); 86 | 87 | $this->sleep(); 88 | 89 | $events = $tracker->getEvents(); 90 | $this->assertCount(0, $events); 91 | 92 | $this->sleep(); 93 | unlink($file); 94 | 95 | $events = $tracker->getEvents(); 96 | $this->assertCount(1, $events); 97 | $this->assertHasResourceEvent($file, FilesystemEvent::DELETE, $events); 98 | 99 | $this->sleep(); 100 | 101 | $events = $tracker->getEvents(); 102 | $this->assertCount(0, $events); 103 | } 104 | 105 | public function testTrackSimpleDirChanges() 106 | { 107 | $tracker = $this->getTracker(); 108 | 109 | mkdir($directory = $this->tmpDir.'/bar'); 110 | 111 | $tracker->track(new TrackedResource('bar', $resource = new DirectoryResource($directory))); 112 | 113 | touch($file1 = $directory.'/new_file'); 114 | $this->sleep(); 115 | 116 | $events = $tracker->getEvents(); 117 | $this->assertCount(1, $events); 118 | $this->assertHasResourceEvent($file1, FilesystemEvent::CREATE, $events); 119 | 120 | touch($file2 = $directory.'/new_file2'); 121 | touch($file3 = $directory.'/new_file3'); 122 | touch($file1); 123 | $this->sleep(); 124 | 125 | $events = $tracker->getEvents(); 126 | $this->assertCount(3, $events); 127 | 128 | $this->assertHasResourceEvent($file1, FilesystemEvent::MODIFY, $events); 129 | $this->assertHasResourceEvent($file2, FilesystemEvent::CREATE, $events); 130 | $this->assertHasResourceEvent($file3, FilesystemEvent::CREATE, $events); 131 | 132 | unlink($file1); 133 | unlink($file3); 134 | $this->sleep(); 135 | 136 | $events = $tracker->getEvents(); 137 | $this->assertCount(2, $events); 138 | 139 | $this->assertHasResourceEvent($file1, FilesystemEvent::DELETE, $events); 140 | $this->assertHasResourceEvent($file3, FilesystemEvent::DELETE, $events); 141 | 142 | unlink($file2); 143 | rmdir($directory); 144 | touch($directory); 145 | $this->sleep(); 146 | 147 | $events = $tracker->getEvents(); 148 | $this->assertCount(2, $events); 149 | $this->assertHasResourceEvent($file2, FilesystemEvent::DELETE, $events); 150 | $this->assertHasResourceEvent($directory, FilesystemEvent::DELETE, $events); 151 | } 152 | 153 | public function testTrackDeepDirChanges() 154 | { 155 | $tracker = $this->getTracker(); 156 | 157 | mkdir($directory1 = $this->tmpDir.'/bar2'); 158 | 159 | $tracker->track( 160 | new TrackedResource('bar2', $resource = new DirectoryResource($directory1)) 161 | ); 162 | 163 | mkdir($directory2 = $directory1.'/subdir'); 164 | touch($file1 = $directory2.'/sub_file'); 165 | $this->sleep(); 166 | 167 | $events = $tracker->getEvents(); 168 | $this->assertCount(2, $events); 169 | 170 | $this->assertHasResourceEvent($directory2, FilesystemEvent::CREATE, $events); 171 | $this->assertHasResourceEvent($file1, FilesystemEvent::CREATE, $events); 172 | 173 | $this->sleep(); 174 | 175 | $events = $tracker->getEvents(); 176 | $this->assertCount(0, $events); 177 | 178 | touch($file2 = $directory1.'/dir1_file.txt'); 179 | touch($file3 = $directory2.'/dir2_file.txt'); 180 | touch($file1); 181 | $this->sleep(); 182 | 183 | $events = $tracker->getEvents(); 184 | $this->assertCount(3, $events); 185 | 186 | $this->assertHasResourceEvent($file1, FilesystemEvent::MODIFY, $events); 187 | $this->assertHasResourceEvent($file2, FilesystemEvent::CREATE, $events); 188 | $this->assertHasResourceEvent($file3, FilesystemEvent::CREATE, $events); 189 | 190 | $this->cleanDir($directory2); 191 | touch($file2); 192 | $this->sleep(); 193 | 194 | $events = $tracker->getEvents(); 195 | $this->assertCount(4, $events); 196 | 197 | $this->assertHasResourceEvent($directory2, FilesystemEvent::DELETE, $events); 198 | $this->assertHasResourceEvent($file1, FilesystemEvent::DELETE, $events); 199 | $this->assertHasResourceEvent($file3, FilesystemEvent::DELETE, $events); 200 | $this->assertHasResourceEvent($file2, FilesystemEvent::MODIFY, $events); 201 | } 202 | 203 | public function testTrackFilteredDirectory() 204 | { 205 | $tracker = $this->getTracker(); 206 | 207 | mkdir($directory1 = $this->tmpDir.'/bar3'); 208 | mkdir($directory2 = $directory1.'/subdir'); 209 | touch($file1 = $directory2.'/sub_file.txt'); 210 | 211 | $tracker->track( 212 | new TrackedResource('bar3', 213 | $resource = new DirectoryResource($directory1, '/\.txt$/') 214 | ) 215 | ); 216 | $this->sleep(); 217 | 218 | touch($file1); 219 | // this file creation should not be notified as it doesn't 220 | // fulfill the directory resource pattern requirement: 221 | touch($file2 = $directory1.'/dir1_file'); 222 | touch($file3 = $directory2.'/dir2_file.txt'); 223 | $this->sleep(); 224 | 225 | $events = $tracker->getEvents(); 226 | $this->assertCount(2, $events); 227 | $this->assertHasResourceEvent($file1, FilesystemEvent::MODIFY, $events); 228 | $this->assertHasResourceEvent($file3, FilesystemEvent::CREATE, $events); 229 | } 230 | 231 | public function testTrackSpecificEvents() 232 | { 233 | $tracker = $this->getTracker(); 234 | 235 | mkdir($directory1 = $this->tmpDir.'/bar3'); 236 | mkdir($directory2 = $directory1.'/subdir'); 237 | touch($file1 = $directory2.'/sub_file.txt'); 238 | touch($file3 = $directory2.'/dir2_file.txt'); 239 | 240 | $tracker->track( 241 | new TrackedResource('bar3', 242 | $resource = new DirectoryResource($directory1, '/\.txt$/') 243 | ), FilesystemEvent::MODIFY | FilesystemEvent::DELETE 244 | ); 245 | $this->sleep(); 246 | 247 | touch($file1); 248 | unlink($file3); 249 | $this->sleep(); 250 | 251 | $events = $tracker->getEvents(); 252 | $this->assertCount(2, $events); 253 | $this->assertHasResourceEvent($file1, FilesystemEvent::MODIFY, $events); 254 | $this->assertHasResourceEvent($file3, FilesystemEvent::DELETE, $events); 255 | } 256 | 257 | protected function assertHasResourceEvent($resource, $type, array $events) 258 | { 259 | $result = array(); 260 | foreach ($events as $event) { 261 | if ($resource === (string) $event->getResource()->getResource()) { 262 | $result[] = $event->getType(); 263 | } 264 | } 265 | 266 | $types = array( 267 | 1 => 'CREATE', 268 | 2 => 'MODIFY', 269 | 4 => 'DELETE', 270 | ); 271 | 272 | if ($result) { 273 | return $this->assertTrue(in_array($type, $result), sprintf('Expected event: %s, actual: %s ', $types[$type], implode(' or ', array_intersect_key($types, array_flip($result))))); 274 | } 275 | 276 | $this->fail(sprintf('Can not find "%s" change event', $resource)); 277 | } 278 | 279 | protected function sleep() 280 | { 281 | usleep($this->getMinimumInterval()); 282 | } 283 | 284 | abstract protected function getMinimumInterval(); 285 | 286 | /** 287 | * @return TrackerInterface 288 | */ 289 | abstract protected function getTracker(); 290 | 291 | protected function cleanDir($dir) 292 | { 293 | if (!is_dir($dir)) { 294 | return; 295 | } 296 | 297 | $flags = \FilesystemIterator::SKIP_DOTS; 298 | $iterator = new \RecursiveDirectoryIterator($dir, $flags); 299 | $iterator = new \RecursiveIteratorIterator( 300 | $iterator, \RecursiveIteratorIterator::CHILD_FIRST 301 | ); 302 | 303 | foreach ($iterator as $path) { 304 | if ($path->isDir()) { 305 | rmdir((string) $path); 306 | } else { 307 | unlink((string) $path); 308 | } 309 | } 310 | 311 | rmdir($dir); 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | add('Lurker\\Tests\\', __DIR__); 5 | $loader->register(); 6 | --------------------------------------------------------------------------------