Skip to content

Instantly share code, notes, and snippets.

@everzet
Created November 29, 2011 22:40
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save everzet/1406938 to your computer and use it in GitHub Desktop.
Save everzet/1406938 to your computer and use it in GitHub Desktop.
Symfony2.1 ResourceWatcher usage example
<?php
$watcher = new Symfony\Component\ResourceWatcher\ResourceWatcher;
// track any change inside directory:
$watcher->track('some/folder1', function($event) {
echo '['.$event->getType().'] '.$event->getResource()."\n"
});
// track only creations inside directory:
$watcher->track('some/folder2', function($event) {
echo $event->getResource()." was created\n"
}, Symfony\Component\ResourceWatcher\Event\Event::CREATED);
// track only *.xml file changes inside directory:
$watcher->track(
new Symfony\Component\Config\Resource\DirectoryResource('some/folder3', '/\.xml$/'),
function($event) use($watcher) {
echo '['.$event->getType().'] '.$event->getResource()."\n"
// stop tracking when first even occurs:
$watcher->stop();
}
);
// start tracking:
$watcher->start();
@schmittjoh
Copy link

How about adding an interface in addition to callables?

<?php

interface FileAlterationListener
{
    function onDirectoryChange($directory);
    function onDirectoryDelete($directory);
    function onDirectoryCreate($directory);
    function onFileChange($file);
    function onFileDelete($file);
    function onFileCreate($file);
    function onWatchStart();
    function onWatchStop();
}

@everzet
Copy link
Author

everzet commented Nov 29, 2011

@schmittjoh it's little bit different - you specify the file filters and event types you wanna track before starting to watch on them and then you just receive events to mapped callable. Nobody will implement interface with 8 methods, when they only need 1 :-)

@schmittjoh
Copy link

Yeah, you can provide an empty implementation for all of them. But callables are not so easily testable like a proper interface, you should care about that :)

@schmittjoh
Copy link

Thinking about it, wouldn't it also be nice to give the watcher a set of directories/files, and then have the listeners triggered for all of them?

@naholyr
Copy link

naholyr commented Nov 30, 2011

👎 for the interface, testing is a false argument to over-complicate things. What you want to test here is if the proper event is triggered if you touch() or unlink(). No need for an interface.

👍 for the possibility to give an array of paths, or even a glob expression.

@schmittjoh
Copy link

Using an object allows to re-use code, properly inject dependencies, etc. testing is one little part that I picked. As a side-effect, we would allow people to use regular services as watchers, and it would even be possible to only have one watch command contrary to separate watch commands for each library that needs one (which isn't very efficient).

Don't get me wrong, I think the closures are very nice for simple things where you want to get something going fast. everzet surely did a good job there. On a framework-scale however, where observers might come from different components/bundles it feels not good enough. Maybe it would make sense to re-use the event dispatcher code?

@everzet
Copy link
Author

everzet commented Nov 30, 2011

@schmittjoh i'm not sure that i have vision of yours. Could you make a proof of concept based on my PR?

@schmittjoh
Copy link

Here is a quick sketch how it might look like. The main difference lies in re-using the existing event dispatcher code.

<?php

class Watcher
{
    private $resources = array();
    private $dispatcher;

    public function __construct(EventDispatcherInterface $dispatcher)
    {
        $this->dispatcher = $dispatcher;
    }

    public function track($directory, $filePattern = null, $caseSensitive = true)
    {
        $this->resources[] = array($directory, $filePattern, $caseSensitive);
    }

    public function start()
    {
        // dispatch events through the EventDispatcher when they occur
    }
}

$dispatcher = new EventDispatcher();
$dispatcher->addListener('watcher.file_changed', array(new MyService(), 'onFileChanged'));
$dispatcher->addListener('watcher.file_changed', array('my.service_id', 'onFileChanged'));
$dispatcher->addListener('watcher.file_changed', function(FileChangeEvent $event) {
    unlink($event->getFile());
    $event->stopPropagation();
});
$dispatcher->addListener('watcher.file_changed', new FilteredListener('some/dir', function(FileChangeEvent $event) {
    // only called for files inside some/dir
}));

$watcher = new ResourceWatcher($dispatcher);
$watcher->track('some/dir');
$watcher->track('some/other/dir', '*.xml')
$watcher->watch();

@everzet
Copy link
Author

everzet commented Nov 30, 2011

I kinda like the idea of reusing EventDispatcher, but i don't like listeners post-filtration and duplication of paths in order watch for a specific path.

Filtration (regex) and tracking parameters should be specified before calling start() in order for component to be efficient and fast. And we shouldn't require users to specify same paths or regexps multiple times in order for component to be usable.

@schmittjoh
Copy link

I agree with you on both points. If we concur that using the EventDispatcher is a good idea, we could start to add some sugar to the raw version above.

a) When used inside Symfony2, we will probably have one watch command (php app/console watcher:start), and all interested components can hook up with this command via tagging respective services.

services:
    my_listener:
        tags:
            - { name: event_listener, event: watcher.file_changed }
            - { name: watcher.resource, dir: some/dir, pattern: *.xml }
            - { name: watcher.resource, dir: another/dir }

This would then be desugarized to the extended version above.

b) When used standalone, we could add a convenience method addListener to the Watcher class which could look like this:

<?php

class Watcher
{
    // ...
    public function addListener($resource, $event, $callable, $priority)
    {
        $this->track($resource);
        $this->dispatcher->addListener($event, new FilteredListener($resource, $callable), $priority);
    }
}

This way we get the best of both worlds. Simple and easy when used standalone and also fits nicely in the Symfony2 Framework context. What do you think?

@everzet
Copy link
Author

everzet commented Nov 30, 2011

What about adding tracking_id to event names? For example:

<?php
$watcher = new Watcher();

// We can require users to specify unique tracking id in `track()` method:
$watcher->track('twig_templates', new DirectoryResource('/some/dir', '/regex/'));

And this way, watcher will send both watcher.twig_templates.file_changed and
watcher.file_changed events for this particular tracking. In this case, user will be able to
choose whether he needs to receieve all or only specific-track events. This will remove
the need in FilteredListener, which complicates things a lot.

@datiecher
Copy link

I guess by now everyone are agreeing on accepting both a callable and an object so I would not jump in on that.

I'm more inclined towards @everzet last option. Way easier to understand and use.

@schmittjoh
Copy link

Yes, sounds good to me as well.

How about reversing the order of arguments function track($resource, $alias = null);?

@everzet
Copy link
Author

everzet commented Nov 30, 2011

Well, there should be 3 parameters at least - $resource, $trackingId and $trackedEvents because there's no reason to track any folder change if user only interested in a file_change

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment