Completed
Push — master ( 1be2e7...d38097 )
by Sam
23s
created

FileFinder::find()   C

Complexity

Conditions 10
Paths 9

Size

Total Lines 52
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 31
nc 9
nop 1
dl 0
loc 52
rs 6.2553
c 0
b 0
f 0

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