Skip to content

Instantly share code, notes, and snippets.

@ifcanduela
Last active July 26, 2018 06:58
Show Gist options
  • Save ifcanduela/70212d833b82a040e54afe5eeaeae28c to your computer and use it in GitHub Desktop.
Save ifcanduela/70212d833b82a040e54afe5eeaeae28c to your computer and use it in GitHub Desktop.
Dead-simple PHP file watcher and task runner.
assets/css/*.less
assets/css/*/*.less
lessc assets/css/app.less app.css
<?php
# Firewatch
# =========
#
# Dead-simple PHP task runner for watching files and folders and reacting to their changes.
#
# This script will read a file called `.watch` with this format:
#
# ```````````````````````````````````````````````
# assets/css/*.less
# assets/css/*/*.less
# lessc assets/css/app.less app.compiled.less
# ```````````````````````````````````````````````
#
# This definition will make the script watch for changes to .less files in assets/css and
# its subfolders and run `lessc` if changes are found.
#
/**
* Class task.
*
* Contains information about a single task: which files to check and
* which command to run.
*/
class Task
{
/** @var string */
protected $command;
/** @var array */
protected $globs;
/** @var string[] */
protected $fileMap = [];
/**
* Create a new watching unit.
*
* @param string $command
* @param string[] $globs
*/
public function __construct($command, $globs)
{
$this->command = $command;
$this->globs = $globs;
}
/**
* Run the task.
*
* @return null
*/
public function run()
{
echo "Running task `{$this->command}`" . PHP_EOL;
shell_exec($this->command);
echo "- - - - - - -" . PHP_EOL;
}
/**
* Check for changes and run the task
* @return bool
*/
public function check()
{
// clear cached timestamps
clearstatcache();
$changed = false;
$fileMap = $this->buildFileMap();
if (count($fileMap) !== count($this->fileMap)) {
// file counts are different
$changed = true;
} elseif (array_diff_key($fileMap, $this->fileMap)) {
// file names are different
$changed = true;
} else {
// file dates are different
$changed = $this->compareFileMap($fileMap);
}
if ($changed) {
// update the file map to the latest version
$this->fileMap = $fileMap;
}
return $changed;
}
private function compareFileMap(array $fileMap)
{
foreach ($fileMap as $fileName => $newTimestamp) {
$oldTimestamp = $this->fileMap[$fileName];
if ($newTimestamp !== $oldTimestamp) {
return true;
}
}
}
private function buildFileMap()
{
$files = [];
foreach ($this->globs as $glob) {
$files += glob($glob);
}
$fileMap = [];
foreach ($files as $filename) {
$fileMap[$filename] = filemtime($filename);
}
return $fileMap;
}
}
/**
* Class Watcher
*
* Keeps track of defined tasks and runs them when necessary.
*/
class Watcher
{
protected $tasks = [];
/**
* Create a file watcher.
*/
public function __construct(string $watchFileName = null)
{
if ($watchFileName) {
$this->buildTasks($watchFileName);
}
}
/**
* Start watching for changes.
*
* @param string $watchFileName
* @return null
*/
public function watch(string $watchFileName = null)
{
if ($watchFileName) {
$this->buildTasks($watchFileName);
}
$this->runTasks();
}
private function buildTasks(string $watchFileName)
{
if (!file_exists($watchFileName)) {
throw new \RuntimeException("Watch definition file not found: `{$watchFileName}`");
}
$globs = [];
$f = fopen($watchFileName, "r");
while ($line = fgets($f)) {
$line = rtrim($line);
if ($line && !in_array($line[0], ["#", " ", "\t"])) {
$globs[] = $line;
}
if ($line[0] === " ") {
$this->tasks[] = new Task(trim($line), $globs);
$globs = [];
}
}
}
private function runTasks()
{
do {
foreach ($this->tasks as $task) {
if ($task->check()) {
$task->run();
}
}
usleep(50000);
} while (true);
}
}
$dotWatchFile = realpath(__DIR__ . DIRECTORY_SEPARATOR . ".watch");
$watcher = new Watcher();
$watcher->watch($dotWatchFile);
// or (new Watcher())->watch(".watch");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment