1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Nette\Utils;
9:
10: use Nette,
11: FilesystemIterator,
12: RecursiveIteratorIterator;
13:
14:
15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26:
27: class Finder extends Nette\Object implements \IteratorAggregate, \Countable
28: {
29:
30: private $paths = array();
31:
32:
33: private $groups;
34:
35:
36: private $exclude = array();
37:
38:
39: private $order = RecursiveIteratorIterator::SELF_FIRST;
40:
41:
42: private $maxDepth = -1;
43:
44:
45: private $cursor;
46:
47:
48: 49: 50: 51: 52:
53: public static function find($mask)
54: {
55: if (!is_array($mask)) {
56: $mask = func_get_args();
57: }
58: $finder = new static;
59: return $finder->select($mask, 'isDir')->select($mask, 'isFile');
60: }
61:
62:
63: 64: 65: 66: 67:
68: public static function findFiles($mask)
69: {
70: if (!is_array($mask)) {
71: $mask = func_get_args();
72: }
73: $finder = new static;
74: return $finder->select($mask, 'isFile');
75: }
76:
77:
78: 79: 80: 81: 82:
83: public static function findDirectories($mask)
84: {
85: if (!is_array($mask)) {
86: $mask = func_get_args();
87: }
88: $finder = new static;
89: return $finder->select($mask, 'isDir');
90: }
91:
92:
93: 94: 95: 96: 97: 98:
99: private function select($masks, $type)
100: {
101: $this->cursor = & $this->groups[];
102: $pattern = self::buildPattern($masks);
103: if ($type || $pattern) {
104: $this->filter(function(FilesystemIterator $file) use ($type, $pattern) {
105: return !$file->isDot()
106: && (!$type || $file->$type())
107: && (!$pattern || preg_match($pattern, '/' . strtr($file->getSubPathName(), '\\', '/')));
108: });
109: }
110: return $this;
111: }
112:
113:
114: 115: 116: 117: 118:
119: public function in($path)
120: {
121: if (!is_array($path)) {
122: $path = func_get_args();
123: }
124: $this->maxDepth = 0;
125: return $this->from($path);
126: }
127:
128:
129: 130: 131: 132: 133:
134: public function from($path)
135: {
136: if ($this->paths) {
137: throw new Nette\InvalidStateException('Directory to search has already been specified.');
138: }
139: if (!is_array($path)) {
140: $path = func_get_args();
141: }
142: $this->paths = $path;
143: $this->cursor = & $this->exclude;
144: return $this;
145: }
146:
147:
148: 149: 150: 151:
152: public function childFirst()
153: {
154: $this->order = RecursiveIteratorIterator::CHILD_FIRST;
155: return $this;
156: }
157:
158:
159: 160: 161: 162: 163:
164: private static function buildPattern($masks)
165: {
166: $pattern = array();
167: foreach ($masks as $mask) {
168: $mask = rtrim(strtr($mask, '\\', '/'), '/');
169: $prefix = '';
170: if ($mask === '') {
171: continue;
172:
173: } elseif ($mask === '*') {
174: return NULL;
175:
176: } elseif ($mask[0] === '/') {
177: $mask = ltrim($mask, '/');
178: $prefix = '(?<=^/)';
179: }
180: $pattern[] = $prefix . strtr(preg_quote($mask, '#'),
181: array('\*\*' => '.*', '\*' => '[^/]*', '\?' => '[^/]', '\[\!' => '[^', '\[' => '[', '\]' => ']', '\-' => '-'));
182: }
183: return $pattern ? '#/(' . implode('|', $pattern) . ')\z#i' : NULL;
184: }
185:
186:
187:
188:
189:
190: 191: 192: 193:
194: public function count()
195: {
196: return iterator_count($this->getIterator());
197: }
198:
199:
200: 201: 202: 203:
204: public function getIterator()
205: {
206: if (!$this->paths) {
207: throw new Nette\InvalidStateException('Call in() or from() to specify directory to search.');
208:
209: } elseif (count($this->paths) === 1) {
210: return $this->buildIterator($this->paths[0]);
211:
212: } else {
213: $iterator = new \AppendIterator();
214: $iterator->append($workaround = new \ArrayIterator(array('workaround PHP bugs #49104, #63077')));
215: foreach ($this->paths as $path) {
216: $iterator->append($this->buildIterator($path));
217: }
218: unset($workaround[0]);
219: return $iterator;
220: }
221: }
222:
223:
224: 225: 226: 227: 228:
229: private function buildIterator($path)
230: {
231: $iterator = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::FOLLOW_SYMLINKS);
232:
233: if ($this->exclude) {
234: $filters = $this->exclude;
235: $iterator = new RecursiveCallbackFilterIterator($iterator, function($foo, $bar, RecursiveCallbackFilterIterator $iterator) use ($filters) {
236: $file = $iterator->getInnerIterator();
237: if (!$file->isDot() && !$file->isFile()) {
238: foreach ($filters as $filter) {
239: if (!call_user_func($filter, $file)) {
240: return FALSE;
241: }
242: }
243: }
244: return TRUE;
245: });
246: }
247:
248: if ($this->maxDepth !== 0) {
249: $iterator = new RecursiveIteratorIterator($iterator, $this->order);
250: $iterator->setMaxDepth($this->maxDepth);
251: }
252:
253: if ($this->groups) {
254: $groups = $this->groups;
255: $iterator = new CallbackFilterIterator($iterator, function($foo, $bar, CallbackFilterIterator $file) use ($groups) {
256: do {
257: $file = $file->getInnerIterator();
258: } while (!$file instanceof FilesystemIterator);
259:
260: foreach ($groups as $filters) {
261: foreach ($filters as $filter) {
262: if (!call_user_func($filter, $file)) {
263: continue 2;
264: }
265: }
266: return TRUE;
267: }
268: return FALSE;
269: });
270: }
271:
272: return $iterator;
273: }
274:
275:
276:
277:
278:
279: 280: 281: 282: 283: 284:
285: public function exclude($masks)
286: {
287: if (!is_array($masks)) {
288: $masks = func_get_args();
289: }
290: $pattern = self::buildPattern($masks);
291: if ($pattern) {
292: $this->filter(function(FilesystemIterator $file) use ($pattern) {
293: return !preg_match($pattern, '/' . strtr($file->getSubPathName(), '\\', '/'));
294: });
295: }
296: return $this;
297: }
298:
299:
300: 301: 302: 303: 304:
305: public function filter($callback)
306: {
307: $this->cursor[] = $callback;
308: return $this;
309: }
310:
311:
312: 313: 314: 315: 316:
317: public function limitDepth($depth)
318: {
319: $this->maxDepth = $depth;
320: return $this;
321: }
322:
323:
324: 325: 326: 327: 328: 329:
330: public function size($operator, $size = NULL)
331: {
332: if (func_num_args() === 1) {
333: if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?((?:\d*\.)?\d+)\s*(K|M|G|)B?\z#i', $operator, $matches)) {
334: throw new Nette\InvalidArgumentException('Invalid size predicate format.');
335: }
336: list(, $operator, $size, $unit) = $matches;
337: static $units = array('' => 1, 'k' => 1e3, 'm' => 1e6, 'g' => 1e9);
338: $size *= $units[strtolower($unit)];
339: $operator = $operator ? $operator : '=';
340: }
341: return $this->filter(function(FilesystemIterator $file) use ($operator, $size) {
342: return Finder::compare($file->getSize(), $operator, $size);
343: });
344: }
345:
346:
347: 348: 349: 350: 351: 352:
353: public function date($operator, $date = NULL)
354: {
355: if (func_num_args() === 1) {
356: if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?(.+)\z#i', $operator, $matches)) {
357: throw new Nette\InvalidArgumentException('Invalid date predicate format.');
358: }
359: list(, $operator, $date) = $matches;
360: $operator = $operator ? $operator : '=';
361: }
362: $date = DateTime::from($date)->format('U');
363: return $this->filter(function(FilesystemIterator $file) use ($operator, $date) {
364: return Finder::compare($file->getMTime(), $operator, $date);
365: });
366: }
367:
368:
369: 370: 371: 372: 373: 374:
375: public static function compare($l, $operator, $r)
376: {
377: switch ($operator) {
378: case '>':
379: return $l > $r;
380: case '>=':
381: return $l >= $r;
382: case '<':
383: return $l < $r;
384: case '<=':
385: return $l <= $r;
386: case '=':
387: case '==':
388: return $l == $r;
389: case '!':
390: case '!=':
391: case '<>':
392: return $l != $r;
393: default:
394: throw new Nette\InvalidArgumentException("Unknown operator $operator.");
395: }
396: }
397:
398: }
399: