Completed
Pull Request — 3 (#8214)
by Ingo
12:52 queued 03:20
created

SS_FileFinder::find()   B

Complexity

Conditions 10
Paths 9

Size

Total Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
nc 9
nop 1
dl 0
loc 45
rs 7.3333
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
	/**
33
	 * @var array
34
	 */
35
	protected static $vcs_dirs = array(
36
		'.git', '.svn', '.hg', '.bzr', 'node_modules',
37
	);
38
39
	/**
40
	 * The default options that are set on a new finder instance. Options not
41
	 * present in this array cannot be set.
42
	 *
43
	 * Any default_option statics defined on child classes are also taken into
44
	 * account.
45
	 *
46
	 * @var array
47
	 */
48
	protected static $default_options = array(
49
		'name_regex'           => null,
50
		'accept_callback'      => null,
51
		'accept_dir_callback'  => null,
52
		'accept_file_callback' => null,
53
		'file_callback'        => null,
54
		'dir_callback'         => null,
55
		'ignore_files'         => null,
56
		'ignore_dirs'          => null,
57
		'ignore_vcs'           => true,
58
		'min_depth'            => null,
59
		'max_depth'            => null
60
	);
61
62
	/**
63
	 * @var array
64
	 */
65
	protected $options;
66
67
	public function __construct() {
68
		$this->options = array();
69
		$class = get_class($this);
70
71
		// We build our options array ourselves, because possibly no class or config manifest exists at this point
72
		do {
73
			$this->options = array_merge(SS_Object::static_lookup($class, 'default_options'), $this->options);
74
		}
75
		while ($class = get_parent_class($class));
76
	}
77
78
	/**
79
	 * Returns an option value set on this instance.
80
	 *
81
	 * @param  string $name
82
	 * @return mixed
83
	 */
84
	public function getOption($name) {
85
		if (!array_key_exists($name, $this->options)) {
86
			throw new InvalidArgumentException("The option $name doesn't exist.");
87
		}
88
89
		return $this->options[$name];
90
	}
91
92
	/**
93
	 * Set an option on this finder instance. See {@link SS_FileFinder} for the
94
	 * list of options available.
95
	 *
96
	 * @param string $name
97
	 * @param mixed $value
98
	 */
99
	public function setOption($name, $value) {
100
		if (!array_key_exists($name, $this->options)) {
101
			throw new InvalidArgumentException("The option $name doesn't exist.");
102
		}
103
104
		$this->options[$name] = $value;
105
	}
106
107
	/**
108
	 * Sets several options at once.
109
	 *
110
	 * @param array $options
111
	 */
112
	public function setOptions(array $options) {
113
		foreach ($options as $k => $v) $this->setOption($k, $v);
114
	}
115
116
	/**
117
	 * Finds all files matching the options within a directory. The search is
118
	 * performed depth first.
119
	 *
120
	 * @param  string $base
121
	 * @return array
122
	 */
123
	public function find($base) {
124
		$paths = array(array(rtrim($base, '/'), 0));
125
		$found = array();
126
127
		$fileCallback = $this->getOption('file_callback');
128
		$dirCallback  = $this->getOption('dir_callback');
129
130
		while ($path = array_shift($paths)) {
131
			list($path, $depth) = $path;
132
133
			foreach (scandir($path) as $basename) {
134
				if ($basename == '.' || $basename == '..') {
135
					continue;
136
				}
137
138
				if (is_dir("$path/$basename")) {
139
					if (!$this->acceptDir($basename, "$path/$basename", $depth + 1)) {
140
						continue;
141
					}
142
143
					if ($dirCallback) {
144
						call_user_func(
145
							$dirCallback, $basename, "$path/$basename", $depth + 1
146
						);
147
					}
148
149
					$paths[] = array("$path/$basename", $depth + 1);
150
				} else {
151
					if (!$this->acceptFile($basename, "$path/$basename", $depth)) {
152
						continue;
153
					}
154
155
					if ($fileCallback) {
156
						call_user_func(
157
							$fileCallback, $basename, "$path/$basename", $depth
158
						);
159
					}
160
161
					$found[] = "$path/$basename";
162
				}
163
			}
164
		}
165
166
		return $found;
167
	}
168
169
	/**
170
	 * Returns TRUE if the directory should be traversed. This can be overloaded
171
	 * to customise functionality, or extended with callbacks.
172
	 *
173
	 * @return bool
174
	 */
175
	protected function acceptDir($basename, $pathname, $depth) {
176
		if ($this->getOption('ignore_vcs') && in_array($basename, self::$vcs_dirs)) {
177
			return false;
178
		}
179
180
		if ($ignore = $this->getOption('ignore_dirs')) {
181
			if (in_array($basename, $ignore)) return false;
182
		}
183
184
		if ($max = $this->getOption('max_depth')) {
185
			if ($depth > $max) return false;
186
		}
187
188
		if ($callback = $this->getOption('accept_callback')) {
189
			if (!call_user_func($callback, $basename, $pathname, $depth)) return false;
190
		}
191
192
		if ($callback = $this->getOption('accept_dir_callback')) {
193
			if (!call_user_func($callback, $basename, $pathname, $depth)) return false;
194
		}
195
196
		return true;
197
	}
198
199
	/**
200
	 * Returns TRUE if the file should be included in the results. This can be
201
	 * overloaded to customise functionality, or extended via callbacks.
202
	 *
203
	 * @return bool
204
	 */
205
	protected function acceptFile($basename, $pathname, $depth) {
206
		if ($regex = $this->getOption('name_regex')) {
207
			if (!preg_match($regex, $basename)) return false;
208
		}
209
210
		if ($ignore = $this->getOption('ignore_files')) {
211
			if (in_array($basename, $ignore)) return false;
212
		}
213
214
		if ($minDepth = $this->getOption('min_depth')) {
215
			if ($depth < $minDepth) return false;
216
		}
217
218
		if ($callback = $this->getOption('accept_callback')) {
219
			if (!call_user_func($callback, $basename, $pathname, $depth)) return false;
220
		}
221
222
		if ($callback = $this->getOption('accept_file_callback')) {
223
			if (!call_user_func($callback, $basename, $pathname, $depth)) return false;
224
		}
225
226
		return true;
227
	}
228
229
}
230