Completed
Push — master ( f5d71d...bfd9cb )
by Sam
12:52
created

FileFinder   B

Complexity

Total Complexity 40

Size/Duplication

Total Lines 206
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 206
rs 8.2608
wmc 40
lcom 1
cbo 1

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 2
A getOption() 0 7 2
A setOption() 0 7 2
A setOptions() 0 3 2
D find() 0 45 10
C acceptDir() 0 23 11
C acceptFile() 0 23 11

How to fix   Complexity   

Complex Class

Complex classes like FileFinder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FileFinder, and based on these observations, apply Extract Interface, too.

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