Parse command line output using Symfony Process - php

Within my Symfony application I need to do several operation with files: list of files from a directory, decrypt them using gpg, parse the output with an external software and encrypt again.
My first question is: is this the right approach for this problem? On another scenario, I'd have written bash/python scripts to do this, but since info (user ids, passphrases, etc) is read from a Symfony API I though it was quite convenient to embed the calls into the application.
My second question is more specific: is there any way to efficiently handle the command line outputs and errors? For instance, when I call 'ls' how can easily convert the output into an array of file names?
private function decryptAction()
{
$user_data_source = '/Users/myuser/datafiles/';
// Scan directory and get a list of all files
$process = new Process('ls ' . $user_data_source);
try {
$process->mustRun();
$files = explode(' ', $process->getOutput());
return $files;
} catch (ProcessFailedException $e) {
return $e->getMessage();
}
}

Found the answer for my second question, but I am still very interested in your thoughts about the entire approach.
// Scan directory and get a list of all files
$process = new Process('ls -1 ' . $user_data_source);
try {
$process->mustRun();
$files = array_filter( explode("\n", $process->getOutput()), 'strlen');
return $files;
} catch (ProcessFailedException $e) {
return $e->getMessage();
}

Unless you really need an immediate response from the call, this kind of tasks are better left to a background process.
So what I would do is write one or more Symfony commands that perform the described processes (read, decrypt, and so on).
Those processes can be executed via crontab, or "daemonized" via another scheduler like Supervisord.
Then, the API call only creates some kind of "semaphore" file that triggers the actual execution, or even better you can use some kind of queue system.

Related

Cleanup console command on any termination

I have the following (simple) lock code for a Laravel 5.3 command:
private $hash = null;
public final function handle() {
try {
$this->hash = md5(serialize([ static::class, $this->arguments(), $this->options() ]));
$this->info("Generated signature ".$this->hash,"v");
if (Redis::exists($this->hash)) {
$this->hash = null;
throw new \Exception("Method ".$this->signature." is already running");
}
Redis::set($this->hash, true);
$this->info("Running method","vv");
$this->runMutuallyExclusiveCommand(); //Actual command is not important here
$this->cleanup();
} catch (\Exception $e) {
$this->error($e->getMessage());
}
}
public function cleanup() {
if (is_string($this->hash)) {
Redis::del($this->hash);
}
}
This works fine if the command is allowed to go through its execution cycle normally (including handling when there's a PHP exception). However the problem arises when the command is interrupted via other means (e.g. CTRL-C or when the terminal window is closed). In that case the cleanup code is not ran and the command is considered to be still "executing" so I need to manually remove the entry from the cache in order to restart it. I have tried running the cleanup code in a __destruct function but that does not seem to be called either.
My question is, is there a way to set some code to be ran when a command is terminated regardless how it was terminated?
Short answer is no. When you kill the running process, either by Ctrl-C or just closing the terminal, you terminate it. You would need to have an interrupt in your shell that links to your cleanup code, but that is way out of scope.
There are other options however. Cron jobs can be run at intermittent intervals to perform clean up tasks and other helpful things. You could also create a start up routine that runs prior to your current code. When you execute the start up routine, it could do the cleanup for you, then call your current routine. I believe your best bet is to use a cron job that simply runs at given intervals that then looks for entries in the cache that are no longer appropriate, and then cleans them. Here is a decent site to get you started with cron jobs https://www.linux.com/learn/scheduling-magic-intro-cron-linux

Configuration File for Driving Selenium

I have about 500 possible paths to a particular page, and I need to test all of them. Each path to the that page looks similar to this (using PHP web driver; usually has about 10 steps):
// Navigate to form
$driver->get('http://www.domain.com');
$driver->get($driver->findElement(WebDriverBy::xpath("//a[contains(text(),'Foo 1')]"))->getAttribute('href'));
$driver->findElement(WebDriverBy::xpath("//div[#class='countryHeader']//a[contains(text(), 'Bar 1')]"))->click();
$driver->findElement(WebDriverBy::xpath("//form[#name='formDisclaimer']//input[contains(#class, 'button')]"))->click();
I don't want to have to write code for all the steps for all possible paths to the page. I do, however, have all the pertinent details of the steps (e.g. the XPath, the string the node may contain, etc.) in a database.
Is there a way for me to "dynamically" produce some sort of configuration file (either in XML or JSON) that I can feed to the driver as a set of instructions for it to follow?
A long time back at one of my project I had a similar requirement. I tried to create a Robot (or someone may call Web Crawler). As I started navigating through the pages I started maintaining the navigation paths in spreadsheet, so I don't have to click on the paths manually. Once I have the paths, next time whenever a Path changes I will be notified and if it is a valid change then make that change in s/s or raise it as a bug.
As you said you have all relevant details in the database then you just can simply read it and in a foreach loop pass to selenium driver.
or if you don't want to have a reference to the database in your test, just dump data to PHP array and add to your test class.
You just need to write a logic to transform your sql data into test. Don't need to write every test manually.
I don't know which testing framework you are using, but you can execute many tests from a single test for example in PHPUnit that would be something like:
class My_PathsTest extends PHPUnit_Framework_TestCase
{
public function setUp() {
// setup $this->tests here
}
public function testAll() {
// $this->tests would contain info about paths taken from database.
$failures = array();
foreach($this->tests as $paths_set) {
try {
/**
* $driver->get($paths_set['start_point']);
* foreach ($paths_set['paths'] as $path ) {
* $driver->findElement(WebDriverBy::xpath($path));
* }
*
* Important!!!
* If you didn't find something you expected
* just throw the PHPUnit_Framework_ExpectationFailedException exception
* throw new PHPUnit_Framework_ExpectationFailedException('Element missing add some info here about which is missing etc..');
*/
}
catch(PHPUnit_Framework_ExpectationFailedException $e) {
$failures[] = $e->getMessage();
}
}
if (!empty($failures)) {
throw new PHPUnit_Framework_ExpectationFailedException(count($failures) . " assertions failed:\n\t" . implode("\n\t", $failures));
}
}
}
best is to get data from db with odbc as a list (array) xpath locators and then loop over it.
If you don't have a direct access to the db, export the query results as a .csv file (MS db has an option save as, not sure about the others) and then read the file and loop over the array

Command pattern in php applications: how to handle controller actions?

I think this is more of a general question (so not php restricted) with regards to ddd and the command pattern.
Let's say I execute a CreatePostCommand from within the create action of my controller, the command will be handled and eventually executed successfully. What's the appropriate way to notify the controller which response to return in case the command did fail or succeed? Given the command handler will fire a domain specific event, I could hook up the controller to the event, but that seems a quite awkward, also not appropriate for every situation (e.g. a post could be created somewhere else and the controller really doesn't know about this :) ).
public function createAction($title, $content)
{
$this->commandBus->execute(new CreatePostCommand($title, $content);
$this->render('…'); // what if the command execution failed?
}
Any thoughts on this?
I think if you are really trying to follow the DDD command pattern then you need to treat the command bus as a fire and forget asynchronous process that may take a long time to complete.
Consider immediately redirecting to a command verifier controller. It's up to the command verifier to actively check the status of the command and see if it worked.
In most cases, the command will have finished successfully and your verifier can then redirect once again to continue normal flow.
If the command fails then the verifier puts up an appropriate error message.
If the command is in progress then you can entire a redirect loop while informing the user that the command is in progress.
Something like:
// Execute the command
$command = new CreatePostCommand($title, $content);
$this->commandBus->execute($command);
return redirect '/command-verifier/' . $command->getId();
// The verification action
public function verifyCommandAction($commandId)
$commandStatus = $this->commandBus->getStatus($commandId);
if ($commandStatus == SUCCESS) redirect to all is well;
if ($commandStatus == FAILED) then oops;
if ($commandStatus == IN_PROGRESS) then maybe pause a bit and redirect again while keeping the user informed.
Clearly there is quite a bit of hand waving going on but I think this is the most general approach especially with php where every request starts from ground zero.
The way I'm currently doing it is as follows (excuse long post).
public function createAction($title, $content) {
try {
$post = $this->commandBus->execute(new CreatePostCommand($title, $content);
}
catch (Exception $e) {
return $this->render('some error template file', $e);
}
return $this->render('successful creation template file', $post);
}
This way, you're creating a post and if everything goes as planned, return the $post object and send that to your view. On the other hand, when an exception is thrown during execution, you catch that error and send it to a view.
My preferred way is to have the controller call a method on a service that manages that behaviour, and have the controller injected as a listener that manages the responses, ie:
public function createAction($title, $content) {
$service = new CreateActionService($title, $content);
return $service->create($this);
}
public function onError(Exception $e) {
return $this->render('some error template file', $e);
}
public function onSuccess($post) {
return $this->render('success', $post);
}
Then in your service...
public function create($listener)
{
try {
$this->commandBus->execute(new CreatePostCommand($title, $content);
}
catch (Exception $e) {
return $this->listener->onError($e);
}
return $this->listener->onSuccess($post);
}
This way your service is managing the various results that the command handler may return, and your controller is left simply to manage the responses that you may wish returned to your presentation layer.

Fast way of detecting directory changes [duplicate]

We have a PHP application, and were thinking it might be advantageous to have the application know if there was a change in its makeup since the last execution. Mainly due to managing caches and such, and knowing that our applications are sometimes accessed by people who don't remember to clear the cache on changes. (Changing the people is the obvious answer, but alas, not really achievable)
We've come up with this, which is the fastest we've managed to eke out, running an average 0.08 on a developer machine for a typical project. We've experimented with shasum,md5 and crc32, and this is the fastest. We are basically md5ing the contents of every file, and md5'ing that output. Security isnt a concern, we're just interested in detecting filesystem changes via a differing checksum.
time (find application/ -path '*/.svn' -prune -o -type f -print0 | xargs -0 md5 | md5)
I suppose the question is, can this be optimised any further?
(I realise that pruning svn will have a cost, but find takes the least amount of time out of the components, so it will be pretty minimal. We're testing this on a working copy atm)
You can be notified of filesystem modifications using the inotify extension.
It can be installed with pecl:
pecl install inotify
Or manually (download, phpize && ./configure && make && make install as usual).
This is a raw binding over the linux inotify syscalls, and is probably the fastest solution on linux.
See this example of a simple tail implementation: http://svn.php.net/viewvc/pecl/inotify/trunk/tail.php?revision=262896&view=markup
If you want a higher level library, or suppot for non-linux systems, take a look at Lurker.
It works on any system, and can use inotity when it's available.
See the example from the README:
$watcher = new ResourceWatcher;
$watcher->track('an arbitrary id', '/path/to/views');
$watcher->addListener('an arbitrary id', function (FilesystemEvent $event) {
echo $event->getResource() . 'was' . $event->getTypeString();
});
$watcher->start();
Instead of going by file contents, you can use the same technique with filename and timestamps:
find . -name '.svn' -prune -o -type f -printf '%m%c%p' | md5sum
This is much faster than reading and hashing the contents of each file.
Insteading of actively searching for changes, why not getting notified when something changes. Have a look at PHP's FAM - File Alteration Monitor API
FAM monitors files and directories, notifying interested applications of changes. More information about FAM is available at » http://oss.sgi.com/projects/fam/. A PHP script may specify a list of files for FAM to monitor using the functions provided by this extension. The FAM process is started when the first connection from any application to it is opened. It exits after all connections to it have been closed.
CAVEAT: requires an additional daemon on the machine and the PECL extension is unmaintained.
We didn't want to use FAM, since we would need to install it on the server, and thats not always possible. Sometimes clients are insistent we deploy on their broken infrastructure. Since it's discontinued, its hard to get that change approved red tape wise also.
The only way to improve the speed of the original version in the question is to make sure your file list is as succinct as possible. IE only hash the directories/files that really matter if changed. Cutting out directories that aren't relevant can give big speed boosts.
Past that, the application was using the function to check if there were changes in order to perform a cache clear if there were. Since we don't really want to halt the application while its doing this, this sort of thing is best farmed out carefully as an asynchronous process using fsockopen. That gives the best 'speed boost' overall, just be careful of race conditions.
Marking this as the 'answer' and upvoting the FAM answer.
since you have svn, why don't you go by revisions. i realise you are skipping svn folders but i suppose you did that for speed advantage and that you do not have modified files in your production servers...
that beeing said, you do not have to re invent the wheel.
you could speed up the process by only looking at metadata read from the directory indexes (modification timestamp, filesize, etc)
you could also stop once you spotted a difference (should theoretically reduce the time by half in average) etc. there is a lot.
i honestly think the best way in this case is to just use the tools already available.
the linux tool diff has a -q option (quick).
you will need to use it with the recursive parameter -r as well.
diff -r -q dir1/ dir2/
it uses a lot of optimisations and i seriously doubt you can significantly improve upon it without considerable effort.
Definitely what you should be using is Inotify its fast and easy to configure, multiple options directly from bash or php of dedicate a simple node-inotify instance for this task
But Inotify does not worn on windows but you can easy write a command line application with FileSystemWatcher or FindFirstChangeNotification and call via exec
If you are looking for only PHP solution then its pretty difficult and you might not get the performance want because the only way is to scan that folder continuously
Here is a Simple Experiment
Don't use in production
Can not manage large file set
Does not support file monitoring
Only Support NEW , DELETED and MODIFIED
Does not support Recursion
Example
if (php_sapi_name() !== 'cli')
die("CLI ONLY");
date_default_timezone_set("America/Los_Angeles");
$sm = new Monitor(__DIR__ . "/test");
// Add hook when new files are created
$sm->hook(function ($file) {
// Send a mail or log to a file
printf("#EMAIL NEW FILE %s\n", $file);
}, Monitor::NOTIFY_NEW);
// Add hook when files are Modified
$sm->hook(function ($file) {
// Do monthing meaningful like calling curl
printf("#HTTP POST MODIFIED FILE %s\n", $file);
}, Monitor::NOTIFY_MODIFIED);
// Add hook when files are Deleted
$sm->hook(function ($file) {
// Crazy ... Send SMS fast or IVR the Boss that you messed up
printf("#SMS DELETED FILE %s\n", $file);
}, Monitor::NOTIFY_DELETED);
// Start Monitor
$sm->start();
Cache Used
// Simpe Cache
// Can be replaced with Memcache
class Cache {
public $f;
function __construct() {
$this->f = fopen("php://temp", "rw+");
}
function get($k) {
rewind($this->f);
return json_decode(stream_get_contents($this->f), true);
}
function set($k, $data) {
fseek($this->f, 0);
fwrite($this->f, json_encode($data));
return $k;
}
function run() {
}
}
The Experiment Class
// The Experiment
class Monitor {
private $dir;
private $info;
private $timeout = 1; // sec
private $timeoutStat = 60; // sec
private $cache;
private $current, $stable, $hook = array();
const NOTIFY_NEW = 1;
const NOTIFY_MODIFIED = 2;
const NOTIFY_DELETED = 4;
const NOTIFY_ALL = 7;
function __construct($dir) {
$this->cache = new Cache();
$this->dir = $dir;
$this->info = new SplFileInfo($this->dir);
$this->scan(true);
}
public function start() {
$i = 0;
$this->stable = (array) $this->cache->get(md5($this->dir));
while(true) {
// Clear System Cache at Intervals
if ($i % $this->timeoutStat == 0) {
clearstatcache();
}
$this->scan(false);
if ($this->stable != $this->current) {
$this->cache->set(md5($this->dir), $this->current);
$this->stable = $this->current;
}
sleep($this->timeout);
$i ++;
// printf("Memory Usage : %0.4f \n", memory_get_peak_usage(false) /
// 1024);
}
}
private function scan($new = false) {
$rdi = new FilesystemIterator($this->dir, FilesystemIterator::SKIP_DOTS);
$this->current = array();
foreach($rdi as $file) {
// Skip files that are not redable
if (! $file->isReadable())
return false;
$path = addslashes($file->getRealPath());
$keyHash = md5($path);
$fileHash = $file->isFile() ? md5_file($path) : "#";
$hash["t"] = $file->getMTime();
$hash["h"] = $fileHash;
$hash["f"] = $path;
$this->current[$keyHash] = json_encode($hash);
}
if ($new === false) {
$this->process();
}
}
public function hook(Callable $call, $type = Monitor::NOTIFY_ALL) {
$this->hook[$type][] = $call;
}
private function process() {
if (isset($this->hook[self::NOTIFY_NEW])) {
$diff = array_flip(array_diff(array_keys($this->current), array_keys($this->stable)));
$this->notify(array_intersect_key($this->current, $diff), self::NOTIFY_NEW);
unset($diff);
}
if (isset($this->hook[self::NOTIFY_DELETED])) {
$deleted = array_flip(array_diff(array_keys($this->stable), array_keys($this->current)));
$this->notify(array_intersect_key($this->stable, $deleted), self::NOTIFY_DELETED);
}
if (isset($this->hook[self::NOTIFY_MODIFIED])) {
$this->notify(array_diff_assoc(array_intersect_key($this->stable, $this->current), array_intersect_key($this->current, $this->stable)), self::NOTIFY_MODIFIED);
}
}
private function notify(array $files, $type) {
if (empty($files))
return;
foreach($this->hook as $t => $hooks) {
if ($t & $type) {
foreach($hooks as $hook) {
foreach($files as $file) {
$info = json_decode($file, true);
$hook($info['f'], $type);
}
}
}
}
}
}

Fastest way to compare directory state, or hashing for fun and profit

We have a PHP application, and were thinking it might be advantageous to have the application know if there was a change in its makeup since the last execution. Mainly due to managing caches and such, and knowing that our applications are sometimes accessed by people who don't remember to clear the cache on changes. (Changing the people is the obvious answer, but alas, not really achievable)
We've come up with this, which is the fastest we've managed to eke out, running an average 0.08 on a developer machine for a typical project. We've experimented with shasum,md5 and crc32, and this is the fastest. We are basically md5ing the contents of every file, and md5'ing that output. Security isnt a concern, we're just interested in detecting filesystem changes via a differing checksum.
time (find application/ -path '*/.svn' -prune -o -type f -print0 | xargs -0 md5 | md5)
I suppose the question is, can this be optimised any further?
(I realise that pruning svn will have a cost, but find takes the least amount of time out of the components, so it will be pretty minimal. We're testing this on a working copy atm)
You can be notified of filesystem modifications using the inotify extension.
It can be installed with pecl:
pecl install inotify
Or manually (download, phpize && ./configure && make && make install as usual).
This is a raw binding over the linux inotify syscalls, and is probably the fastest solution on linux.
See this example of a simple tail implementation: http://svn.php.net/viewvc/pecl/inotify/trunk/tail.php?revision=262896&view=markup
If you want a higher level library, or suppot for non-linux systems, take a look at Lurker.
It works on any system, and can use inotity when it's available.
See the example from the README:
$watcher = new ResourceWatcher;
$watcher->track('an arbitrary id', '/path/to/views');
$watcher->addListener('an arbitrary id', function (FilesystemEvent $event) {
echo $event->getResource() . 'was' . $event->getTypeString();
});
$watcher->start();
Instead of going by file contents, you can use the same technique with filename and timestamps:
find . -name '.svn' -prune -o -type f -printf '%m%c%p' | md5sum
This is much faster than reading and hashing the contents of each file.
Insteading of actively searching for changes, why not getting notified when something changes. Have a look at PHP's FAM - File Alteration Monitor API
FAM monitors files and directories, notifying interested applications of changes. More information about FAM is available at » http://oss.sgi.com/projects/fam/. A PHP script may specify a list of files for FAM to monitor using the functions provided by this extension. The FAM process is started when the first connection from any application to it is opened. It exits after all connections to it have been closed.
CAVEAT: requires an additional daemon on the machine and the PECL extension is unmaintained.
We didn't want to use FAM, since we would need to install it on the server, and thats not always possible. Sometimes clients are insistent we deploy on their broken infrastructure. Since it's discontinued, its hard to get that change approved red tape wise also.
The only way to improve the speed of the original version in the question is to make sure your file list is as succinct as possible. IE only hash the directories/files that really matter if changed. Cutting out directories that aren't relevant can give big speed boosts.
Past that, the application was using the function to check if there were changes in order to perform a cache clear if there were. Since we don't really want to halt the application while its doing this, this sort of thing is best farmed out carefully as an asynchronous process using fsockopen. That gives the best 'speed boost' overall, just be careful of race conditions.
Marking this as the 'answer' and upvoting the FAM answer.
since you have svn, why don't you go by revisions. i realise you are skipping svn folders but i suppose you did that for speed advantage and that you do not have modified files in your production servers...
that beeing said, you do not have to re invent the wheel.
you could speed up the process by only looking at metadata read from the directory indexes (modification timestamp, filesize, etc)
you could also stop once you spotted a difference (should theoretically reduce the time by half in average) etc. there is a lot.
i honestly think the best way in this case is to just use the tools already available.
the linux tool diff has a -q option (quick).
you will need to use it with the recursive parameter -r as well.
diff -r -q dir1/ dir2/
it uses a lot of optimisations and i seriously doubt you can significantly improve upon it without considerable effort.
Definitely what you should be using is Inotify its fast and easy to configure, multiple options directly from bash or php of dedicate a simple node-inotify instance for this task
But Inotify does not worn on windows but you can easy write a command line application with FileSystemWatcher or FindFirstChangeNotification and call via exec
If you are looking for only PHP solution then its pretty difficult and you might not get the performance want because the only way is to scan that folder continuously
Here is a Simple Experiment
Don't use in production
Can not manage large file set
Does not support file monitoring
Only Support NEW , DELETED and MODIFIED
Does not support Recursion
Example
if (php_sapi_name() !== 'cli')
die("CLI ONLY");
date_default_timezone_set("America/Los_Angeles");
$sm = new Monitor(__DIR__ . "/test");
// Add hook when new files are created
$sm->hook(function ($file) {
// Send a mail or log to a file
printf("#EMAIL NEW FILE %s\n", $file);
}, Monitor::NOTIFY_NEW);
// Add hook when files are Modified
$sm->hook(function ($file) {
// Do monthing meaningful like calling curl
printf("#HTTP POST MODIFIED FILE %s\n", $file);
}, Monitor::NOTIFY_MODIFIED);
// Add hook when files are Deleted
$sm->hook(function ($file) {
// Crazy ... Send SMS fast or IVR the Boss that you messed up
printf("#SMS DELETED FILE %s\n", $file);
}, Monitor::NOTIFY_DELETED);
// Start Monitor
$sm->start();
Cache Used
// Simpe Cache
// Can be replaced with Memcache
class Cache {
public $f;
function __construct() {
$this->f = fopen("php://temp", "rw+");
}
function get($k) {
rewind($this->f);
return json_decode(stream_get_contents($this->f), true);
}
function set($k, $data) {
fseek($this->f, 0);
fwrite($this->f, json_encode($data));
return $k;
}
function run() {
}
}
The Experiment Class
// The Experiment
class Monitor {
private $dir;
private $info;
private $timeout = 1; // sec
private $timeoutStat = 60; // sec
private $cache;
private $current, $stable, $hook = array();
const NOTIFY_NEW = 1;
const NOTIFY_MODIFIED = 2;
const NOTIFY_DELETED = 4;
const NOTIFY_ALL = 7;
function __construct($dir) {
$this->cache = new Cache();
$this->dir = $dir;
$this->info = new SplFileInfo($this->dir);
$this->scan(true);
}
public function start() {
$i = 0;
$this->stable = (array) $this->cache->get(md5($this->dir));
while(true) {
// Clear System Cache at Intervals
if ($i % $this->timeoutStat == 0) {
clearstatcache();
}
$this->scan(false);
if ($this->stable != $this->current) {
$this->cache->set(md5($this->dir), $this->current);
$this->stable = $this->current;
}
sleep($this->timeout);
$i ++;
// printf("Memory Usage : %0.4f \n", memory_get_peak_usage(false) /
// 1024);
}
}
private function scan($new = false) {
$rdi = new FilesystemIterator($this->dir, FilesystemIterator::SKIP_DOTS);
$this->current = array();
foreach($rdi as $file) {
// Skip files that are not redable
if (! $file->isReadable())
return false;
$path = addslashes($file->getRealPath());
$keyHash = md5($path);
$fileHash = $file->isFile() ? md5_file($path) : "#";
$hash["t"] = $file->getMTime();
$hash["h"] = $fileHash;
$hash["f"] = $path;
$this->current[$keyHash] = json_encode($hash);
}
if ($new === false) {
$this->process();
}
}
public function hook(Callable $call, $type = Monitor::NOTIFY_ALL) {
$this->hook[$type][] = $call;
}
private function process() {
if (isset($this->hook[self::NOTIFY_NEW])) {
$diff = array_flip(array_diff(array_keys($this->current), array_keys($this->stable)));
$this->notify(array_intersect_key($this->current, $diff), self::NOTIFY_NEW);
unset($diff);
}
if (isset($this->hook[self::NOTIFY_DELETED])) {
$deleted = array_flip(array_diff(array_keys($this->stable), array_keys($this->current)));
$this->notify(array_intersect_key($this->stable, $deleted), self::NOTIFY_DELETED);
}
if (isset($this->hook[self::NOTIFY_MODIFIED])) {
$this->notify(array_diff_assoc(array_intersect_key($this->stable, $this->current), array_intersect_key($this->current, $this->stable)), self::NOTIFY_MODIFIED);
}
}
private function notify(array $files, $type) {
if (empty($files))
return;
foreach($this->hook as $t => $hooks) {
if ($t & $type) {
foreach($hooks as $hook) {
foreach($files as $file) {
$info = json_decode($file, true);
$hook($info['f'], $type);
}
}
}
}
}
}

Categories