<?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; } }