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