Skip to content
This repository
Fetching contributors…

Octocat-spinner-32-eaf2f5

Cannot retrieve contributors at this time

file 230 lines (196 sloc) 6.328 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
<?php
/**
* A utility class that finds any files matching a set of rules that are
* present within a directory tree.
*
* Each file finder instance can have several options set on it:
* - name_regex (string): A regular expression that file basenames must match.
* - accept_callback (callback): A callback that is called to accept a file.
* If it returns false the item will be skipped. The callback is passed the
* basename, pathname and depth.
* - accept_dir_callback (callback): The same as accept_callback, but only
* called for directories.
* - accept_file_callback (callback): The same as accept_callback, but only
* called for files.
* - file_callback (callback): A callback that is called when a file i
* succesfully matched. It is passed the basename, pathname and depth.
* - dir_callback (callback): The same as file_callback, but called for
* directories.
* - ignore_files (array): An array of file names to skip.
* - ignore_dirs (array): An array of directory names to skip.
* - ignore_vcs (bool): Skip over commonly used VCS dirs (svn, git, hg, bzr).
* This is enabled by default. The names of VCS directories to skip over
* are defined in {@link SS_FileFInder::$vcs_dirs}.
* - max_depth (int): The maxmium depth to traverse down the folder tree,
* default to unlimited.
*
* @package framework
* @subpackage filesystem
*/
class SS_FileFinder {

/**
* @var array
*/
public static $vcs_dirs = array(
'.git', '.svn', '.hg', '.bzr'
);

/**
* The default options that are set on a new finder instance. Options not
* present in this array cannot be set.
*
* Any default_option statics defined on child classes are also taken into
* account.
*
* @var array
*/
public static $default_options = array(
'name_regex' => null,
'accept_callback' => null,
'accept_dir_callback' => null,
'accept_file_callback' => null,
'file_callback' => null,
'dir_callback' => null,
'ignore_files' => null,
'ignore_dirs' => null,
'ignore_vcs' => true,
'min_depth' => null,
'max_depth' => null
);

/**
* @var array
*/
protected $options;

public function __construct() {
$this->options = array();
$class = get_class($this);

// We build our options array ourselves, because possibly no class or config manifest exists at this point
do {
$this->options = array_merge(Object::static_lookup($class, 'default_options'), $this->options);
}
while ($class = get_parent_class($class));
}

/**
* Returns an option value set on this instance.
*
* @param string $name
* @return mixed
*/
public function getOption($name) {
if (!array_key_exists($name, $this->options)) {
throw new InvalidArgumentException("The option $name doesn't exist.");
}

return $this->options[$name];
}

/**
* Set an option on this finder instance. See {@link SS_FileFinder} for the
* list of options available.
*
* @param string $name
* @param mixed $value
*/
public function setOption($name, $value) {
if (!array_key_exists($name, $this->options)) {
throw new InvalidArgumentException("The option $name doesn't exist.");
}

$this->options[$name] = $value;
}

/**
* Sets several options at once.
*
* @param array $options
*/
public function setOptions(array $options) {
foreach ($options as $k => $v) $this->setOption($k, $v);
}

/**
* Finds all files matching the options within a directory. The search is
* performed depth first.
*
* @param string $base
* @return array
*/
public function find($base) {
$paths = array(array(rtrim($base, '/'), 0));
$found = array();

$fileCallback = $this->getOption('file_callback');
$dirCallback = $this->getOption('dir_callback');

while ($path = array_shift($paths)) {
list($path, $depth) = $path;

foreach (scandir($path) as $basename) {
if ($basename == '.' || $basename == '..') {
continue;
}

if (is_dir("$path/$basename")) {
if (!$this->acceptDir($basename, "$path/$basename", $depth + 1)) {
continue;
}

if ($dirCallback) {
call_user_func(
$dirCallback, $basename, "$path/$basename", $depth + 1
);
}

$paths[] = array("$path/$basename", $depth + 1);
} else {
if (!$this->acceptFile($basename, "$path/$basename", $depth)) {
continue;
}

if ($fileCallback) {
call_user_func(
$fileCallback, $basename, "$path/$basename", $depth
);
}

$found[] = "$path/$basename";
}
}
}

return $found;
}

/**
* Returns TRUE if the directory should be traversed. This can be overloaded
* to customise functionality, or extended with callbacks.
*
* @return bool
*/
protected function acceptDir($basename, $pathname, $depth) {
if ($this->getOption('ignore_vcs') && in_array($basename, self::$vcs_dirs)) {
return false;
}

if ($ignore = $this->getOption('ignore_dirs')) {
if (in_array($basename, $ignore)) return false;
}

if ($max = $this->getOption('max_depth')) {
if ($depth > $max) return false;
}

if ($callback = $this->getOption('accept_callback')) {
if (!call_user_func($callback, $basename, $pathname, $depth)) return false;
}

if ($callback = $this->getOption('accept_dir_callback')) {
if (!call_user_func($callback, $basename, $pathname, $depth)) return false;
}

return true;
}

/**
* Returns TRUE if the file should be included in the results. This can be
* overloaded to customise functionality, or extended via callbacks.
*
* @return bool
*/
protected function acceptFile($basename, $pathname, $depth) {
if ($regex = $this->getOption('name_regex')) {
if (!preg_match($regex, $basename)) return false;
}

if ($ignore = $this->getOption('ignore_files')) {
if (in_array($basename, $ignore)) return false;
}

if ($minDepth = $this->getOption('min_depth')) {
if ($depth < $minDepth) return false;
}

if ($callback = $this->getOption('accept_callback')) {
if (!call_user_func($callback, $basename, $pathname, $depth)) return false;
}

if ($callback = $this->getOption('accept_file_callback')) {
if (!call_user_func($callback, $basename, $pathname, $depth)) return false;
}

return true;
}

}
Something went wrong with that request. Please try again.