1: <?php
2:
3: /*
4: * This file is part of the Symfony package.
5: *
6: * (c) Fabien Potencier <fabien@symfony.com>
7: *
8: * For the full copyright and license information, please view the LICENSE
9: * file that was distributed with this source code.
10: */
11:
12: namespace Symfony\Component\Finder;
13:
14: use Symfony\Component\Finder\Adapter\AdapterInterface;
15: use Symfony\Component\Finder\Adapter\GnuFindAdapter;
16: use Symfony\Component\Finder\Adapter\BsdFindAdapter;
17: use Symfony\Component\Finder\Adapter\PhpAdapter;
18: use Symfony\Component\Finder\Exception\ExceptionInterface;
19:
20: /**
21: * Finder allows to build rules to find files and directories.
22: *
23: * It is a thin wrapper around several specialized iterator classes.
24: *
25: * All rules may be invoked several times.
26: *
27: * All methods return the current Finder object to allow easy chaining:
28: *
29: * $finder = Finder::create()->files()->name('*.php')->in(__DIR__);
30: *
31: * @author Fabien Potencier <fabien@symfony.com>
32: *
33: * @api
34: */
35: class Finder implements \IteratorAggregate, \Countable
36: {
37: const IGNORE_VCS_FILES = 1;
38: const IGNORE_DOT_FILES = 2;
39:
40: private $mode = 0;
41: private $names = array();
42: private $notNames = array();
43: private $exclude = array();
44: private $filters = array();
45: private $depths = array();
46: private $sizes = array();
47: private $followLinks = false;
48: private $sort = false;
49: private $ignore = 0;
50: private $dirs = array();
51: private $dates = array();
52: private $iterators = array();
53: private $contains = array();
54: private $notContains = array();
55: private $adapters = array();
56: private $paths = array();
57: private $notPaths = array();
58:
59: private static $vcsPatterns = array('.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg');
60:
61: /**
62: * Constructor.
63: */
64: public function __construct()
65: {
66: $this->ignore = static::IGNORE_VCS_FILES | static::IGNORE_DOT_FILES;
67:
68: $this
69: ->addAdapter(new GnuFindAdapter())
70: ->addAdapter(new BsdFindAdapter())
71: ->addAdapter(new PhpAdapter(), -50)
72: ->setAdapter('php')
73: ;
74: }
75:
76: /**
77: * Creates a new Finder.
78: *
79: * @return Finder A new Finder instance
80: *
81: * @api
82: */
83: public static function create()
84: {
85: return new static();
86: }
87:
88: /**
89: * Registers a finder engine implementation.
90: *
91: * @param AdapterInterface $adapter An adapter instance
92: * @param integer $priority Highest is selected first
93: *
94: * @return Finder The current Finder instance
95: */
96: public function addAdapter(Adapter\AdapterInterface $adapter, $priority = 0)
97: {
98: $this->adapters[$adapter->getName()] = array(
99: 'adapter' => $adapter,
100: 'priority' => $priority,
101: 'selected' => false,
102: );
103:
104: return $this->sortAdapters();
105: }
106:
107: /**
108: * Sets the selected adapter to the best one according to the current platform the code is run on.
109: *
110: * @return Finder The current Finder instance
111: */
112: public function useBestAdapter()
113: {
114: $this->resetAdapterSelection();
115:
116: return $this->sortAdapters();
117: }
118:
119: /**
120: * Selects the adapter to use.
121: *
122: * @param string $name
123: *
124: * @throws \InvalidArgumentException
125: *
126: * @return Finder The current Finder instance
127: */
128: public function setAdapter($name)
129: {
130: if (!isset($this->adapters[$name])) {
131: throw new \InvalidArgumentException(sprintf('Adapter "%s" does not exist.', $name));
132: }
133:
134: $this->resetAdapterSelection();
135: $this->adapters[$name]['selected'] = true;
136:
137: return $this->sortAdapters();
138: }
139:
140: /**
141: * Removes all adapters registered in the finder.
142: *
143: * @return Finder The current Finder instance
144: */
145: public function removeAdapters()
146: {
147: $this->adapters = array();
148:
149: return $this;
150: }
151:
152: /**
153: * Returns registered adapters ordered by priority without extra information.
154: *
155: * @return AdapterInterface[]
156: */
157: public function getAdapters()
158: {
159: return array_values(array_map(function(array $adapter) {
160: return $adapter['adapter'];
161: }, $this->adapters));
162: }
163:
164: /**
165: * Restricts the matching to directories only.
166: *
167: * @return Finder The current Finder instance
168: *
169: * @api
170: */
171: public function directories()
172: {
173: $this->mode = Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES;
174:
175: return $this;
176: }
177:
178: /**
179: * Restricts the matching to files only.
180: *
181: * @return Finder The current Finder instance
182: *
183: * @api
184: */
185: public function files()
186: {
187: $this->mode = Iterator\FileTypeFilterIterator::ONLY_FILES;
188:
189: return $this;
190: }
191:
192: /**
193: * Adds tests for the directory depth.
194: *
195: * Usage:
196: *
197: * $finder->depth('> 1') // the Finder will start matching at level 1.
198: * $finder->depth('< 3') // the Finder will descend at most 3 levels of directories below the starting point.
199: *
200: * @param int $level The depth level expression
201: *
202: * @return Finder The current Finder instance
203: *
204: * @see Symfony\Component\Finder\Iterator\DepthRangeFilterIterator
205: * @see Symfony\Component\Finder\Comparator\NumberComparator
206: *
207: * @api
208: */
209: public function depth($level)
210: {
211: $this->depths[] = new Comparator\NumberComparator($level);
212:
213: return $this;
214: }
215:
216: /**
217: * Adds tests for file dates (last modified).
218: *
219: * The date must be something that strtotime() is able to parse:
220: *
221: * $finder->date('since yesterday');
222: * $finder->date('until 2 days ago');
223: * $finder->date('> now - 2 hours');
224: * $finder->date('>= 2005-10-15');
225: *
226: * @param string $date A date rage string
227: *
228: * @return Finder The current Finder instance
229: *
230: * @see strtotime
231: * @see Symfony\Component\Finder\Iterator\DateRangeFilterIterator
232: * @see Symfony\Component\Finder\Comparator\DateComparator
233: *
234: * @api
235: */
236: public function date($date)
237: {
238: $this->dates[] = new Comparator\DateComparator($date);
239:
240: return $this;
241: }
242:
243: /**
244: * Adds rules that files must match.
245: *
246: * You can use patterns (delimited with / sign), globs or simple strings.
247: *
248: * $finder->name('*.php')
249: * $finder->name('/\.php$/') // same as above
250: * $finder->name('test.php')
251: *
252: * @param string $pattern A pattern (a regexp, a glob, or a string)
253: *
254: * @return Finder The current Finder instance
255: *
256: * @see Symfony\Component\Finder\Iterator\FilenameFilterIterator
257: *
258: * @api
259: */
260: public function name($pattern)
261: {
262: $this->names[] = $pattern;
263:
264: return $this;
265: }
266:
267: /**
268: * Adds rules that files must not match.
269: *
270: * @param string $pattern A pattern (a regexp, a glob, or a string)
271: *
272: * @return Finder The current Finder instance
273: *
274: * @see Symfony\Component\Finder\Iterator\FilenameFilterIterator
275: *
276: * @api
277: */
278: public function notName($pattern)
279: {
280: $this->notNames[] = $pattern;
281:
282: return $this;
283: }
284:
285: /**
286: * Adds tests that file contents must match.
287: *
288: * Strings or PCRE patterns can be used:
289: *
290: * $finder->contains('Lorem ipsum')
291: * $finder->contains('/Lorem ipsum/i')
292: *
293: * @param string $pattern A pattern (string or regexp)
294: *
295: * @return Finder The current Finder instance
296: *
297: * @see Symfony\Component\Finder\Iterator\FilecontentFilterIterator
298: */
299: public function contains($pattern)
300: {
301: $this->contains[] = $pattern;
302:
303: return $this;
304: }
305:
306: /**
307: * Adds tests that file contents must not match.
308: *
309: * Strings or PCRE patterns can be used:
310: *
311: * $finder->notContains('Lorem ipsum')
312: * $finder->notContains('/Lorem ipsum/i')
313: *
314: * @param string $pattern A pattern (string or regexp)
315: *
316: * @return Finder The current Finder instance
317: *
318: * @see Symfony\Component\Finder\Iterator\FilecontentFilterIterator
319: */
320: public function notContains($pattern)
321: {
322: $this->notContains[] = $pattern;
323:
324: return $this;
325: }
326:
327: /**
328: * Adds rules that filenames must match.
329: *
330: * You can use patterns (delimited with / sign) or simple strings.
331: *
332: * $finder->path('some/special/dir')
333: * $finder->path('/some\/special\/dir/') // same as above
334: *
335: * Use only / as dirname separator.
336: *
337: * @param string $pattern A pattern (a regexp or a string)
338: *
339: * @return Finder The current Finder instance
340: *
341: * @see Symfony\Component\Finder\Iterator\FilenameFilterIterator
342: */
343: public function path($pattern)
344: {
345: $this->paths[] = $pattern;
346:
347: return $this;
348: }
349:
350: /**
351: * Adds rules that filenames must not match.
352: *
353: * You can use patterns (delimited with / sign) or simple strings.
354: *
355: * $finder->notPath('some/special/dir')
356: * $finder->notPath('/some\/special\/dir/') // same as above
357: *
358: * Use only / as dirname separator.
359: *
360: * @param string $pattern A pattern (a regexp or a string)
361: *
362: * @return Finder The current Finder instance
363: *
364: * @see Symfony\Component\Finder\Iterator\FilenameFilterIterator
365: */
366: public function notPath($pattern)
367: {
368: $this->notPaths[] = $pattern;
369:
370: return $this;
371: }
372:
373: /**
374: * Adds tests for file sizes.
375: *
376: * $finder->size('> 10K');
377: * $finder->size('<= 1Ki');
378: * $finder->size(4);
379: *
380: * @param string $size A size range string
381: *
382: * @return Finder The current Finder instance
383: *
384: * @see Symfony\Component\Finder\Iterator\SizeRangeFilterIterator
385: * @see Symfony\Component\Finder\Comparator\NumberComparator
386: *
387: * @api
388: */
389: public function size($size)
390: {
391: $this->sizes[] = new Comparator\NumberComparator($size);
392:
393: return $this;
394: }
395:
396: /**
397: * Excludes directories.
398: *
399: * @param string|array $dirs A directory path or an array of directories
400: *
401: * @return Finder The current Finder instance
402: *
403: * @see Symfony\Component\Finder\Iterator\ExcludeDirectoryFilterIterator
404: *
405: * @api
406: */
407: public function exclude($dirs)
408: {
409: $this->exclude = array_merge($this->exclude, (array) $dirs);
410:
411: return $this;
412: }
413:
414: /**
415: * Excludes "hidden" directories and files (starting with a dot).
416: *
417: * @param Boolean $ignoreDotFiles Whether to exclude "hidden" files or not
418: *
419: * @return Finder The current Finder instance
420: *
421: * @see Symfony\Component\Finder\Iterator\ExcludeDirectoryFilterIterator
422: *
423: * @api
424: */
425: public function ignoreDotFiles($ignoreDotFiles)
426: {
427: if ($ignoreDotFiles) {
428: $this->ignore = $this->ignore | static::IGNORE_DOT_FILES;
429: } else {
430: $this->ignore = $this->ignore & ~static::IGNORE_DOT_FILES;
431: }
432:
433: return $this;
434: }
435:
436: /**
437: * Forces the finder to ignore version control directories.
438: *
439: * @param Boolean $ignoreVCS Whether to exclude VCS files or not
440: *
441: * @return Finder The current Finder instance
442: *
443: * @see Symfony\Component\Finder\Iterator\ExcludeDirectoryFilterIterator
444: *
445: * @api
446: */
447: public function ignoreVCS($ignoreVCS)
448: {
449: if ($ignoreVCS) {
450: $this->ignore = $this->ignore | static::IGNORE_VCS_FILES;
451: } else {
452: $this->ignore = $this->ignore & ~static::IGNORE_VCS_FILES;
453: }
454:
455: return $this;
456: }
457:
458: /**
459: * Adds VCS patterns.
460: *
461: * @see ignoreVCS
462: *
463: * @param string|string[] $pattern VCS patterns to ignore
464: */
465: public static function addVCSPattern($pattern)
466: {
467: foreach ((array) $pattern as $p) {
468: self::$vcsPatterns[] = $p;
469: }
470:
471: self::$vcsPatterns = array_unique(self::$vcsPatterns);
472: }
473:
474: /**
475: * Sorts files and directories by an anonymous function.
476: *
477: * The anonymous function receives two \SplFileInfo instances to compare.
478: *
479: * This can be slow as all the matching files and directories must be retrieved for comparison.
480: *
481: * @param \Closure $closure An anonymous function
482: *
483: * @return Finder The current Finder instance
484: *
485: * @see Symfony\Component\Finder\Iterator\SortableIterator
486: *
487: * @api
488: */
489: public function sort(\Closure $closure)
490: {
491: $this->sort = $closure;
492:
493: return $this;
494: }
495:
496: /**
497: * Sorts files and directories by name.
498: *
499: * This can be slow as all the matching files and directories must be retrieved for comparison.
500: *
501: * @return Finder The current Finder instance
502: *
503: * @see Symfony\Component\Finder\Iterator\SortableIterator
504: *
505: * @api
506: */
507: public function sortByName()
508: {
509: $this->sort = Iterator\SortableIterator::SORT_BY_NAME;
510:
511: return $this;
512: }
513:
514: /**
515: * Sorts files and directories by type (directories before files), then by name.
516: *
517: * This can be slow as all the matching files and directories must be retrieved for comparison.
518: *
519: * @return Finder The current Finder instance
520: *
521: * @see Symfony\Component\Finder\Iterator\SortableIterator
522: *
523: * @api
524: */
525: public function sortByType()
526: {
527: $this->sort = Iterator\SortableIterator::SORT_BY_TYPE;
528:
529: return $this;
530: }
531:
532: /**
533: * Sorts files and directories by the last accessed time.
534: *
535: * This is the time that the file was last accessed, read or written to.
536: *
537: * This can be slow as all the matching files and directories must be retrieved for comparison.
538: *
539: * @return Finder The current Finder instance
540: *
541: * @see Symfony\Component\Finder\Iterator\SortableIterator
542: *
543: * @api
544: */
545: public function sortByAccessedTime()
546: {
547: $this->sort = Iterator\SortableIterator::SORT_BY_ACCESSED_TIME;
548:
549: return $this;
550: }
551:
552: /**
553: * Sorts files and directories by the last inode changed time.
554: *
555: * This is the time that the inode information was last modified (permissions, owner, group or other metadata).
556: *
557: * On Windows, since inode is not available, changed time is actually the file creation time.
558: *
559: * This can be slow as all the matching files and directories must be retrieved for comparison.
560: *
561: * @return Finder The current Finder instance
562: *
563: * @see Symfony\Component\Finder\Iterator\SortableIterator
564: *
565: * @api
566: */
567: public function sortByChangedTime()
568: {
569: $this->sort = Iterator\SortableIterator::SORT_BY_CHANGED_TIME;
570:
571: return $this;
572: }
573:
574: /**
575: * Sorts files and directories by the last modified time.
576: *
577: * This is the last time the actual contents of the file were last modified.
578: *
579: * This can be slow as all the matching files and directories must be retrieved for comparison.
580: *
581: * @return Finder The current Finder instance
582: *
583: * @see Symfony\Component\Finder\Iterator\SortableIterator
584: *
585: * @api
586: */
587: public function sortByModifiedTime()
588: {
589: $this->sort = Iterator\SortableIterator::SORT_BY_MODIFIED_TIME;
590:
591: return $this;
592: }
593:
594: /**
595: * Filters the iterator with an anonymous function.
596: *
597: * The anonymous function receives a \SplFileInfo and must return false
598: * to remove files.
599: *
600: * @param \Closure $closure An anonymous function
601: *
602: * @return Finder The current Finder instance
603: *
604: * @see Symfony\Component\Finder\Iterator\CustomFilterIterator
605: *
606: * @api
607: */
608: public function filter(\Closure $closure)
609: {
610: $this->filters[] = $closure;
611:
612: return $this;
613: }
614:
615: /**
616: * Forces the following of symlinks.
617: *
618: * @return Finder The current Finder instance
619: *
620: * @api
621: */
622: public function followLinks()
623: {
624: $this->followLinks = true;
625:
626: return $this;
627: }
628:
629: /**
630: * Searches files and directories which match defined rules.
631: *
632: * @param string|array $dirs A directory path or an array of directories
633: *
634: * @return Finder The current Finder instance
635: *
636: * @throws \InvalidArgumentException if one of the directories does not exist
637: *
638: * @api
639: */
640: public function in($dirs)
641: {
642: $resolvedDirs = array();
643:
644: foreach ((array) $dirs as $dir) {
645: if (is_dir($dir)) {
646: $resolvedDirs[] = $dir;
647: } elseif ($glob = glob($dir, GLOB_ONLYDIR)) {
648: $resolvedDirs = array_merge($resolvedDirs, $glob);
649: } else {
650: throw new \InvalidArgumentException(sprintf('The "%s" directory does not exist.', $dir));
651: }
652: }
653:
654: $this->dirs = array_merge($this->dirs, $resolvedDirs);
655:
656: return $this;
657: }
658:
659: /**
660: * Returns an Iterator for the current Finder configuration.
661: *
662: * This method implements the IteratorAggregate interface.
663: *
664: * @return \Iterator An iterator
665: *
666: * @throws \LogicException if the in() method has not been called
667: */
668: public function getIterator()
669: {
670: if (0 === count($this->dirs) && 0 === count($this->iterators)) {
671: throw new \LogicException('You must call one of in() or append() methods before iterating over a Finder.');
672: }
673:
674: if (1 === count($this->dirs) && 0 === count($this->iterators)) {
675: return $this->searchInDirectory($this->dirs[0]);
676: }
677:
678: $iterator = new \AppendIterator();
679: foreach ($this->dirs as $dir) {
680: $iterator->append($this->searchInDirectory($dir));
681: }
682:
683: foreach ($this->iterators as $it) {
684: $iterator->append($it);
685: }
686:
687: return $iterator;
688: }
689:
690: /**
691: * Appends an existing set of files/directories to the finder.
692: *
693: * The set can be another Finder, an Iterator, an IteratorAggregate, or even a plain array.
694: *
695: * @param mixed $iterator
696: *
697: * @return Finder The finder
698: *
699: * @throws \InvalidArgumentException When the given argument is not iterable.
700: */
701: public function append($iterator)
702: {
703: if ($iterator instanceof \IteratorAggregate) {
704: $this->iterators[] = $iterator->getIterator();
705: } elseif ($iterator instanceof \Iterator) {
706: $this->iterators[] = $iterator;
707: } elseif ($iterator instanceof \Traversable || is_array($iterator)) {
708: $it = new \ArrayIterator();
709: foreach ($iterator as $file) {
710: $it->append($file instanceof \SplFileInfo ? $file : new \SplFileInfo($file));
711: }
712: $this->iterators[] = $it;
713: } else {
714: throw new \InvalidArgumentException('Finder::append() method wrong argument type.');
715: }
716:
717: return $this;
718: }
719:
720: /**
721: * Counts all the results collected by the iterators.
722: *
723: * @return int
724: */
725: public function count()
726: {
727: return iterator_count($this->getIterator());
728: }
729:
730: /**
731: * @return Finder The current Finder instance
732: */
733: private function sortAdapters()
734: {
735: uasort($this->adapters, function (array $a, array $b) {
736: if ($a['selected'] || $b['selected']) {
737: return $a['selected'] ? -1 : 1;
738: }
739:
740: return $a['priority'] > $b['priority'] ? -1 : 1;
741: });
742:
743: return $this;
744: }
745:
746: /**
747: * @param $dir
748: *
749: * @return \Iterator
750: *
751: * @throws \RuntimeException When none of the adapters are supported
752: */
753: private function searchInDirectory($dir)
754: {
755: if (static::IGNORE_VCS_FILES === (static::IGNORE_VCS_FILES & $this->ignore)) {
756: $this->exclude = array_merge($this->exclude, self::$vcsPatterns);
757: }
758:
759: if (static::IGNORE_DOT_FILES === (static::IGNORE_DOT_FILES & $this->ignore)) {
760: $this->notPaths[] = '#(^|/)\..+(/|$)#';
761: }
762:
763: foreach ($this->adapters as $adapter) {
764: if ($adapter['adapter']->isSupported()) {
765: try {
766: return $this
767: ->buildAdapter($adapter['adapter'])
768: ->searchInDirectory($dir);
769: } catch (ExceptionInterface $e) {}
770: }
771: }
772:
773: throw new \RuntimeException('No supported adapter found.');
774: }
775:
776: /**
777: * @param AdapterInterface $adapter
778: *
779: * @return AdapterInterface
780: */
781: private function buildAdapter(AdapterInterface $adapter)
782: {
783: return $adapter
784: ->setFollowLinks($this->followLinks)
785: ->setDepths($this->depths)
786: ->setMode($this->mode)
787: ->setExclude($this->exclude)
788: ->setNames($this->names)
789: ->setNotNames($this->notNames)
790: ->setContains($this->contains)
791: ->setNotContains($this->notContains)
792: ->setSizes($this->sizes)
793: ->setDates($this->dates)
794: ->setFilters($this->filters)
795: ->setSort($this->sort)
796: ->setPath($this->paths)
797: ->setNotPath($this->notPaths);
798: }
799:
800: /**
801: * Unselects all adapters.
802: */
803: private function resetAdapterSelection()
804: {
805: $this->adapters = array_map(function (array $properties) {
806: $properties['selected'] = false;
807:
808: return $properties;
809: }, $this->adapters);
810: }
811: }
812: