/filesystem/FileFinder.php

https://github.com/oddnoc/silverstripe-framework · PHP · 229 lines · 124 code · 31 blank · 74 comment · 33 complexity · 0caf27e667eaeae7f529c355f0cee560 MD5 · raw file

  1. <?php
  2. /**
  3. * A utility class that finds any files matching a set of rules that are
  4. * present within a directory tree.
  5. *
  6. * Each file finder instance can have several options set on it:
  7. * - name_regex (string): A regular expression that file basenames must match.
  8. * - accept_callback (callback): A callback that is called to accept a file.
  9. * If it returns false the item will be skipped. The callback is passed the
  10. * basename, pathname and depth.
  11. * - accept_dir_callback (callback): The same as accept_callback, but only
  12. * called for directories.
  13. * - accept_file_callback (callback): The same as accept_callback, but only
  14. * called for files.
  15. * - file_callback (callback): A callback that is called when a file i
  16. * succesfully matched. It is passed the basename, pathname and depth.
  17. * - dir_callback (callback): The same as file_callback, but called for
  18. * directories.
  19. * - ignore_files (array): An array of file names to skip.
  20. * - ignore_dirs (array): An array of directory names to skip.
  21. * - ignore_vcs (bool): Skip over commonly used VCS dirs (svn, git, hg, bzr).
  22. * This is enabled by default. The names of VCS directories to skip over
  23. * are defined in {@link SS_FileFInder::$vcs_dirs}.
  24. * - max_depth (int): The maxmium depth to traverse down the folder tree,
  25. * default to unlimited.
  26. *
  27. * @package framework
  28. * @subpackage filesystem
  29. */
  30. class SS_FileFinder {
  31. /**
  32. * @var array
  33. */
  34. protected static $vcs_dirs = array(
  35. '.git', '.svn', '.hg', '.bzr', 'node_modules',
  36. );
  37. /**
  38. * The default options that are set on a new finder instance. Options not
  39. * present in this array cannot be set.
  40. *
  41. * Any default_option statics defined on child classes are also taken into
  42. * account.
  43. *
  44. * @var array
  45. */
  46. protected static $default_options = array(
  47. 'name_regex' => null,
  48. 'accept_callback' => null,
  49. 'accept_dir_callback' => null,
  50. 'accept_file_callback' => null,
  51. 'file_callback' => null,
  52. 'dir_callback' => null,
  53. 'ignore_files' => null,
  54. 'ignore_dirs' => null,
  55. 'ignore_vcs' => true,
  56. 'min_depth' => null,
  57. 'max_depth' => null
  58. );
  59. /**
  60. * @var array
  61. */
  62. protected $options;
  63. public function __construct() {
  64. $this->options = array();
  65. $class = get_class($this);
  66. // We build our options array ourselves, because possibly no class or config manifest exists at this point
  67. do {
  68. $this->options = array_merge(SS_Object::static_lookup($class, 'default_options'), $this->options);
  69. }
  70. while ($class = get_parent_class($class));
  71. }
  72. /**
  73. * Returns an option value set on this instance.
  74. *
  75. * @param string $name
  76. * @return mixed
  77. */
  78. public function getOption($name) {
  79. if (!array_key_exists($name, $this->options)) {
  80. throw new InvalidArgumentException("The option $name doesn't exist.");
  81. }
  82. return $this->options[$name];
  83. }
  84. /**
  85. * Set an option on this finder instance. See {@link SS_FileFinder} for the
  86. * list of options available.
  87. *
  88. * @param string $name
  89. * @param mixed $value
  90. */
  91. public function setOption($name, $value) {
  92. if (!array_key_exists($name, $this->options)) {
  93. throw new InvalidArgumentException("The option $name doesn't exist.");
  94. }
  95. $this->options[$name] = $value;
  96. }
  97. /**
  98. * Sets several options at once.
  99. *
  100. * @param array $options
  101. */
  102. public function setOptions(array $options) {
  103. foreach ($options as $k => $v) $this->setOption($k, $v);
  104. }
  105. /**
  106. * Finds all files matching the options within a directory. The search is
  107. * performed depth first.
  108. *
  109. * @param string $base
  110. * @return array
  111. */
  112. public function find($base) {
  113. $paths = array(array(rtrim($base, '/'), 0));
  114. $found = array();
  115. $fileCallback = $this->getOption('file_callback');
  116. $dirCallback = $this->getOption('dir_callback');
  117. while ($path = array_shift($paths)) {
  118. list($path, $depth) = $path;
  119. foreach (scandir($path) as $basename) {
  120. if ($basename == '.' || $basename == '..') {
  121. continue;
  122. }
  123. if (is_dir("$path/$basename")) {
  124. if (!$this->acceptDir($basename, "$path/$basename", $depth + 1)) {
  125. continue;
  126. }
  127. if ($dirCallback) {
  128. call_user_func(
  129. $dirCallback, $basename, "$path/$basename", $depth + 1
  130. );
  131. }
  132. $paths[] = array("$path/$basename", $depth + 1);
  133. } else {
  134. if (!$this->acceptFile($basename, "$path/$basename", $depth)) {
  135. continue;
  136. }
  137. if ($fileCallback) {
  138. call_user_func(
  139. $fileCallback, $basename, "$path/$basename", $depth
  140. );
  141. }
  142. $found[] = "$path/$basename";
  143. }
  144. }
  145. }
  146. return $found;
  147. }
  148. /**
  149. * Returns TRUE if the directory should be traversed. This can be overloaded
  150. * to customise functionality, or extended with callbacks.
  151. *
  152. * @return bool
  153. */
  154. protected function acceptDir($basename, $pathname, $depth) {
  155. if ($this->getOption('ignore_vcs') && in_array($basename, self::$vcs_dirs)) {
  156. return false;
  157. }
  158. if ($ignore = $this->getOption('ignore_dirs')) {
  159. if (in_array($basename, $ignore)) return false;
  160. }
  161. if ($max = $this->getOption('max_depth')) {
  162. if ($depth > $max) return false;
  163. }
  164. if ($callback = $this->getOption('accept_callback')) {
  165. if (!call_user_func($callback, $basename, $pathname, $depth)) return false;
  166. }
  167. if ($callback = $this->getOption('accept_dir_callback')) {
  168. if (!call_user_func($callback, $basename, $pathname, $depth)) return false;
  169. }
  170. return true;
  171. }
  172. /**
  173. * Returns TRUE if the file should be included in the results. This can be
  174. * overloaded to customise functionality, or extended via callbacks.
  175. *
  176. * @return bool
  177. */
  178. protected function acceptFile($basename, $pathname, $depth) {
  179. if ($regex = $this->getOption('name_regex')) {
  180. if (!preg_match($regex, $basename)) return false;
  181. }
  182. if ($ignore = $this->getOption('ignore_files')) {
  183. if (in_array($basename, $ignore)) return false;
  184. }
  185. if ($minDepth = $this->getOption('min_depth')) {
  186. if ($depth < $minDepth) return false;
  187. }
  188. if ($callback = $this->getOption('accept_callback')) {
  189. if (!call_user_func($callback, $basename, $pathname, $depth)) return false;
  190. }
  191. if ($callback = $this->getOption('accept_file_callback')) {
  192. if (!call_user_func($callback, $basename, $pathname, $depth)) return false;
  193. }
  194. return true;
  195. }
  196. }