Finder.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Finder;
  11. use Symfony\Component\Finder\Comparator\DateComparator;
  12. use Symfony\Component\Finder\Comparator\NumberComparator;
  13. use Symfony\Component\Finder\Iterator\CustomFilterIterator;
  14. use Symfony\Component\Finder\Iterator\DateRangeFilterIterator;
  15. use Symfony\Component\Finder\Iterator\DepthRangeFilterIterator;
  16. use Symfony\Component\Finder\Iterator\ExcludeDirectoryFilterIterator;
  17. use Symfony\Component\Finder\Iterator\FilecontentFilterIterator;
  18. use Symfony\Component\Finder\Iterator\FilenameFilterIterator;
  19. use Symfony\Component\Finder\Iterator\SizeRangeFilterIterator;
  20. use Symfony\Component\Finder\Iterator\SortableIterator;
  21. /**
  22. * Finder allows to build rules to find files and directories.
  23. *
  24. * It is a thin wrapper around several specialized iterator classes.
  25. *
  26. * All rules may be invoked several times.
  27. *
  28. * All methods return the current Finder object to allow easy chaining:
  29. *
  30. * $finder = Finder::create()->files()->name('*.php')->in(__DIR__);
  31. *
  32. * @author Fabien Potencier <fabien@symfony.com>
  33. */
  34. class Finder implements \IteratorAggregate, \Countable
  35. {
  36. const IGNORE_VCS_FILES = 1;
  37. const IGNORE_DOT_FILES = 2;
  38. private $mode = 0;
  39. private $names = [];
  40. private $notNames = [];
  41. private $exclude = [];
  42. private $filters = [];
  43. private $depths = [];
  44. private $sizes = [];
  45. private $followLinks = false;
  46. private $reverseSorting = false;
  47. private $sort = false;
  48. private $ignore = 0;
  49. private $dirs = [];
  50. private $dates = [];
  51. private $iterators = [];
  52. private $contains = [];
  53. private $notContains = [];
  54. private $paths = [];
  55. private $notPaths = [];
  56. private $ignoreUnreadableDirs = false;
  57. private static $vcsPatterns = ['.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg'];
  58. public function __construct()
  59. {
  60. $this->ignore = static::IGNORE_VCS_FILES | static::IGNORE_DOT_FILES;
  61. }
  62. /**
  63. * Creates a new Finder.
  64. *
  65. * @return static
  66. */
  67. public static function create()
  68. {
  69. return new static();
  70. }
  71. /**
  72. * Restricts the matching to directories only.
  73. *
  74. * @return $this
  75. */
  76. public function directories()
  77. {
  78. $this->mode = Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES;
  79. return $this;
  80. }
  81. /**
  82. * Restricts the matching to files only.
  83. *
  84. * @return $this
  85. */
  86. public function files()
  87. {
  88. $this->mode = Iterator\FileTypeFilterIterator::ONLY_FILES;
  89. return $this;
  90. }
  91. /**
  92. * Adds tests for the directory depth.
  93. *
  94. * Usage:
  95. *
  96. * $finder->depth('> 1') // the Finder will start matching at level 1.
  97. * $finder->depth('< 3') // the Finder will descend at most 3 levels of directories below the starting point.
  98. * $finder->depth(['>= 1', '< 3'])
  99. *
  100. * @param string|int|string[]|int[] $levels The depth level expression or an array of depth levels
  101. *
  102. * @return $this
  103. *
  104. * @see DepthRangeFilterIterator
  105. * @see NumberComparator
  106. */
  107. public function depth($levels)
  108. {
  109. foreach ((array) $levels as $level) {
  110. $this->depths[] = new Comparator\NumberComparator($level);
  111. }
  112. return $this;
  113. }
  114. /**
  115. * Adds tests for file dates (last modified).
  116. *
  117. * The date must be something that strtotime() is able to parse:
  118. *
  119. * $finder->date('since yesterday');
  120. * $finder->date('until 2 days ago');
  121. * $finder->date('> now - 2 hours');
  122. * $finder->date('>= 2005-10-15');
  123. * $finder->date(['>= 2005-10-15', '<= 2006-05-27']);
  124. *
  125. * @param string|string[] $dates A date range string or an array of date ranges
  126. *
  127. * @return $this
  128. *
  129. * @see strtotime
  130. * @see DateRangeFilterIterator
  131. * @see DateComparator
  132. */
  133. public function date($dates)
  134. {
  135. foreach ((array) $dates as $date) {
  136. $this->dates[] = new Comparator\DateComparator($date);
  137. }
  138. return $this;
  139. }
  140. /**
  141. * Adds rules that files must match.
  142. *
  143. * You can use patterns (delimited with / sign), globs or simple strings.
  144. *
  145. * $finder->name('*.php')
  146. * $finder->name('/\.php$/') // same as above
  147. * $finder->name('test.php')
  148. * $finder->name(['test.py', 'test.php'])
  149. *
  150. * @param string|string[] $patterns A pattern (a regexp, a glob, or a string) or an array of patterns
  151. *
  152. * @return $this
  153. *
  154. * @see FilenameFilterIterator
  155. */
  156. public function name($patterns)
  157. {
  158. $this->names = \array_merge($this->names, (array) $patterns);
  159. return $this;
  160. }
  161. /**
  162. * Adds rules that files must not match.
  163. *
  164. * @param string|string[] $patterns A pattern (a regexp, a glob, or a string) or an array of patterns
  165. *
  166. * @return $this
  167. *
  168. * @see FilenameFilterIterator
  169. */
  170. public function notName($patterns)
  171. {
  172. $this->notNames = \array_merge($this->notNames, (array) $patterns);
  173. return $this;
  174. }
  175. /**
  176. * Adds tests that file contents must match.
  177. *
  178. * Strings or PCRE patterns can be used:
  179. *
  180. * $finder->contains('Lorem ipsum')
  181. * $finder->contains('/Lorem ipsum/i')
  182. * $finder->contains(['dolor', '/ipsum/i'])
  183. *
  184. * @param string|string[] $patterns A pattern (string or regexp) or an array of patterns
  185. *
  186. * @return $this
  187. *
  188. * @see FilecontentFilterIterator
  189. */
  190. public function contains($patterns)
  191. {
  192. $this->contains = \array_merge($this->contains, (array) $patterns);
  193. return $this;
  194. }
  195. /**
  196. * Adds tests that file contents must not match.
  197. *
  198. * Strings or PCRE patterns can be used:
  199. *
  200. * $finder->notContains('Lorem ipsum')
  201. * $finder->notContains('/Lorem ipsum/i')
  202. * $finder->notContains(['lorem', '/dolor/i'])
  203. *
  204. * @param string|string[] $patterns A pattern (string or regexp) or an array of patterns
  205. *
  206. * @return $this
  207. *
  208. * @see FilecontentFilterIterator
  209. */
  210. public function notContains($patterns)
  211. {
  212. $this->notContains = \array_merge($this->notContains, (array) $patterns);
  213. return $this;
  214. }
  215. /**
  216. * Adds rules that filenames must match.
  217. *
  218. * You can use patterns (delimited with / sign) or simple strings.
  219. *
  220. * $finder->path('some/special/dir')
  221. * $finder->path('/some\/special\/dir/') // same as above
  222. * $finder->path(['some dir', 'another/dir'])
  223. *
  224. * Use only / as dirname separator.
  225. *
  226. * @param string|string[] $patterns A pattern (a regexp or a string) or an array of patterns
  227. *
  228. * @return $this
  229. *
  230. * @see FilenameFilterIterator
  231. */
  232. public function path($patterns)
  233. {
  234. $this->paths = \array_merge($this->paths, (array) $patterns);
  235. return $this;
  236. }
  237. /**
  238. * Adds rules that filenames must not match.
  239. *
  240. * You can use patterns (delimited with / sign) or simple strings.
  241. *
  242. * $finder->notPath('some/special/dir')
  243. * $finder->notPath('/some\/special\/dir/') // same as above
  244. * $finder->notPath(['some/file.txt', 'another/file.log'])
  245. *
  246. * Use only / as dirname separator.
  247. *
  248. * @param string|string[] $patterns A pattern (a regexp or a string) or an array of patterns
  249. *
  250. * @return $this
  251. *
  252. * @see FilenameFilterIterator
  253. */
  254. public function notPath($patterns)
  255. {
  256. $this->notPaths = \array_merge($this->notPaths, (array) $patterns);
  257. return $this;
  258. }
  259. /**
  260. * Adds tests for file sizes.
  261. *
  262. * $finder->size('> 10K');
  263. * $finder->size('<= 1Ki');
  264. * $finder->size(4);
  265. * $finder->size(['> 10K', '< 20K'])
  266. *
  267. * @param string|int|string[]|int[] $sizes A size range string or an integer or an array of size ranges
  268. *
  269. * @return $this
  270. *
  271. * @see SizeRangeFilterIterator
  272. * @see NumberComparator
  273. */
  274. public function size($sizes)
  275. {
  276. foreach ((array) $sizes as $size) {
  277. $this->sizes[] = new Comparator\NumberComparator($size);
  278. }
  279. return $this;
  280. }
  281. /**
  282. * Excludes directories.
  283. *
  284. * Directories passed as argument must be relative to the ones defined with the `in()` method. For example:
  285. *
  286. * $finder->in(__DIR__)->exclude('ruby');
  287. *
  288. * @param string|array $dirs A directory path or an array of directories
  289. *
  290. * @return $this
  291. *
  292. * @see ExcludeDirectoryFilterIterator
  293. */
  294. public function exclude($dirs)
  295. {
  296. $this->exclude = array_merge($this->exclude, (array) $dirs);
  297. return $this;
  298. }
  299. /**
  300. * Excludes "hidden" directories and files (starting with a dot).
  301. *
  302. * This option is enabled by default.
  303. *
  304. * @param bool $ignoreDotFiles Whether to exclude "hidden" files or not
  305. *
  306. * @return $this
  307. *
  308. * @see ExcludeDirectoryFilterIterator
  309. */
  310. public function ignoreDotFiles($ignoreDotFiles)
  311. {
  312. if ($ignoreDotFiles) {
  313. $this->ignore |= static::IGNORE_DOT_FILES;
  314. } else {
  315. $this->ignore &= ~static::IGNORE_DOT_FILES;
  316. }
  317. return $this;
  318. }
  319. /**
  320. * Forces the finder to ignore version control directories.
  321. *
  322. * This option is enabled by default.
  323. *
  324. * @param bool $ignoreVCS Whether to exclude VCS files or not
  325. *
  326. * @return $this
  327. *
  328. * @see ExcludeDirectoryFilterIterator
  329. */
  330. public function ignoreVCS($ignoreVCS)
  331. {
  332. if ($ignoreVCS) {
  333. $this->ignore |= static::IGNORE_VCS_FILES;
  334. } else {
  335. $this->ignore &= ~static::IGNORE_VCS_FILES;
  336. }
  337. return $this;
  338. }
  339. /**
  340. * Adds VCS patterns.
  341. *
  342. * @see ignoreVCS()
  343. *
  344. * @param string|string[] $pattern VCS patterns to ignore
  345. */
  346. public static function addVCSPattern($pattern)
  347. {
  348. foreach ((array) $pattern as $p) {
  349. self::$vcsPatterns[] = $p;
  350. }
  351. self::$vcsPatterns = array_unique(self::$vcsPatterns);
  352. }
  353. /**
  354. * Sorts files and directories by an anonymous function.
  355. *
  356. * The anonymous function receives two \SplFileInfo instances to compare.
  357. *
  358. * This can be slow as all the matching files and directories must be retrieved for comparison.
  359. *
  360. * @return $this
  361. *
  362. * @see SortableIterator
  363. */
  364. public function sort(\Closure $closure)
  365. {
  366. $this->sort = $closure;
  367. return $this;
  368. }
  369. /**
  370. * Sorts files and directories by name.
  371. *
  372. * This can be slow as all the matching files and directories must be retrieved for comparison.
  373. *
  374. * @param bool $useNaturalSort Whether to use natural sort or not, disabled by default
  375. *
  376. * @return $this
  377. *
  378. * @see SortableIterator
  379. */
  380. public function sortByName(/* bool $useNaturalSort = false */)
  381. {
  382. if (\func_num_args() < 1 && __CLASS__ !== \get_class($this) && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface) {
  383. @trigger_error(sprintf('The "%s()" method will have a new "bool $useNaturalSort = false" argument in version 5.0, not defining it is deprecated since Symfony 4.2.', __METHOD__), E_USER_DEPRECATED);
  384. }
  385. $useNaturalSort = 0 < \func_num_args() && func_get_arg(0);
  386. $this->sort = $useNaturalSort ? Iterator\SortableIterator::SORT_BY_NAME_NATURAL : Iterator\SortableIterator::SORT_BY_NAME;
  387. return $this;
  388. }
  389. /**
  390. * Sorts files and directories by type (directories before files), then by name.
  391. *
  392. * This can be slow as all the matching files and directories must be retrieved for comparison.
  393. *
  394. * @return $this
  395. *
  396. * @see SortableIterator
  397. */
  398. public function sortByType()
  399. {
  400. $this->sort = Iterator\SortableIterator::SORT_BY_TYPE;
  401. return $this;
  402. }
  403. /**
  404. * Sorts files and directories by the last accessed time.
  405. *
  406. * This is the time that the file was last accessed, read or written to.
  407. *
  408. * This can be slow as all the matching files and directories must be retrieved for comparison.
  409. *
  410. * @return $this
  411. *
  412. * @see SortableIterator
  413. */
  414. public function sortByAccessedTime()
  415. {
  416. $this->sort = Iterator\SortableIterator::SORT_BY_ACCESSED_TIME;
  417. return $this;
  418. }
  419. /**
  420. * Reverses the sorting.
  421. *
  422. * @return $this
  423. */
  424. public function reverseSorting()
  425. {
  426. $this->reverseSorting = true;
  427. return $this;
  428. }
  429. /**
  430. * Sorts files and directories by the last inode changed time.
  431. *
  432. * This is the time that the inode information was last modified (permissions, owner, group or other metadata).
  433. *
  434. * On Windows, since inode is not available, changed time is actually the file creation time.
  435. *
  436. * This can be slow as all the matching files and directories must be retrieved for comparison.
  437. *
  438. * @return $this
  439. *
  440. * @see SortableIterator
  441. */
  442. public function sortByChangedTime()
  443. {
  444. $this->sort = Iterator\SortableIterator::SORT_BY_CHANGED_TIME;
  445. return $this;
  446. }
  447. /**
  448. * Sorts files and directories by the last modified time.
  449. *
  450. * This is the last time the actual contents of the file were last modified.
  451. *
  452. * This can be slow as all the matching files and directories must be retrieved for comparison.
  453. *
  454. * @return $this
  455. *
  456. * @see SortableIterator
  457. */
  458. public function sortByModifiedTime()
  459. {
  460. $this->sort = Iterator\SortableIterator::SORT_BY_MODIFIED_TIME;
  461. return $this;
  462. }
  463. /**
  464. * Filters the iterator with an anonymous function.
  465. *
  466. * The anonymous function receives a \SplFileInfo and must return false
  467. * to remove files.
  468. *
  469. * @return $this
  470. *
  471. * @see CustomFilterIterator
  472. */
  473. public function filter(\Closure $closure)
  474. {
  475. $this->filters[] = $closure;
  476. return $this;
  477. }
  478. /**
  479. * Forces the following of symlinks.
  480. *
  481. * @return $this
  482. */
  483. public function followLinks()
  484. {
  485. $this->followLinks = true;
  486. return $this;
  487. }
  488. /**
  489. * Tells finder to ignore unreadable directories.
  490. *
  491. * By default, scanning unreadable directories content throws an AccessDeniedException.
  492. *
  493. * @param bool $ignore
  494. *
  495. * @return $this
  496. */
  497. public function ignoreUnreadableDirs($ignore = true)
  498. {
  499. $this->ignoreUnreadableDirs = (bool) $ignore;
  500. return $this;
  501. }
  502. /**
  503. * Searches files and directories which match defined rules.
  504. *
  505. * @param string|array $dirs A directory path or an array of directories
  506. *
  507. * @return $this
  508. *
  509. * @throws \InvalidArgumentException if one of the directories does not exist
  510. */
  511. public function in($dirs)
  512. {
  513. $resolvedDirs = [];
  514. foreach ((array) $dirs as $dir) {
  515. if (is_dir($dir)) {
  516. $resolvedDirs[] = $this->normalizeDir($dir);
  517. } elseif ($glob = glob($dir, (\defined('GLOB_BRACE') ? GLOB_BRACE : 0) | GLOB_ONLYDIR)) {
  518. $resolvedDirs = array_merge($resolvedDirs, array_map([$this, 'normalizeDir'], $glob));
  519. } else {
  520. throw new \InvalidArgumentException(sprintf('The "%s" directory does not exist.', $dir));
  521. }
  522. }
  523. $this->dirs = array_merge($this->dirs, $resolvedDirs);
  524. return $this;
  525. }
  526. /**
  527. * Returns an Iterator for the current Finder configuration.
  528. *
  529. * This method implements the IteratorAggregate interface.
  530. *
  531. * @return \Iterator|SplFileInfo[] An iterator
  532. *
  533. * @throws \LogicException if the in() method has not been called
  534. */
  535. public function getIterator()
  536. {
  537. if (0 === \count($this->dirs) && 0 === \count($this->iterators)) {
  538. throw new \LogicException('You must call one of in() or append() methods before iterating over a Finder.');
  539. }
  540. if (1 === \count($this->dirs) && 0 === \count($this->iterators)) {
  541. return $this->searchInDirectory($this->dirs[0]);
  542. }
  543. $iterator = new \AppendIterator();
  544. foreach ($this->dirs as $dir) {
  545. $iterator->append($this->searchInDirectory($dir));
  546. }
  547. foreach ($this->iterators as $it) {
  548. $iterator->append($it);
  549. }
  550. return $iterator;
  551. }
  552. /**
  553. * Appends an existing set of files/directories to the finder.
  554. *
  555. * The set can be another Finder, an Iterator, an IteratorAggregate, or even a plain array.
  556. *
  557. * @param iterable $iterator
  558. *
  559. * @return $this
  560. *
  561. * @throws \InvalidArgumentException when the given argument is not iterable
  562. */
  563. public function append($iterator)
  564. {
  565. if ($iterator instanceof \IteratorAggregate) {
  566. $this->iterators[] = $iterator->getIterator();
  567. } elseif ($iterator instanceof \Iterator) {
  568. $this->iterators[] = $iterator;
  569. } elseif ($iterator instanceof \Traversable || \is_array($iterator)) {
  570. $it = new \ArrayIterator();
  571. foreach ($iterator as $file) {
  572. $it->append($file instanceof \SplFileInfo ? $file : new \SplFileInfo($file));
  573. }
  574. $this->iterators[] = $it;
  575. } else {
  576. throw new \InvalidArgumentException('Finder::append() method wrong argument type.');
  577. }
  578. return $this;
  579. }
  580. /**
  581. * Check if the any results were found.
  582. *
  583. * @return bool
  584. */
  585. public function hasResults()
  586. {
  587. foreach ($this->getIterator() as $_) {
  588. return true;
  589. }
  590. return false;
  591. }
  592. /**
  593. * Counts all the results collected by the iterators.
  594. *
  595. * @return int
  596. */
  597. public function count()
  598. {
  599. return iterator_count($this->getIterator());
  600. }
  601. private function searchInDirectory(string $dir): \Iterator
  602. {
  603. if (static::IGNORE_VCS_FILES === (static::IGNORE_VCS_FILES & $this->ignore)) {
  604. $this->exclude = array_merge($this->exclude, self::$vcsPatterns);
  605. }
  606. if (static::IGNORE_DOT_FILES === (static::IGNORE_DOT_FILES & $this->ignore)) {
  607. $this->notPaths[] = '#(^|/)\..+(/|$)#';
  608. }
  609. $minDepth = 0;
  610. $maxDepth = PHP_INT_MAX;
  611. foreach ($this->depths as $comparator) {
  612. switch ($comparator->getOperator()) {
  613. case '>':
  614. $minDepth = $comparator->getTarget() + 1;
  615. break;
  616. case '>=':
  617. $minDepth = $comparator->getTarget();
  618. break;
  619. case '<':
  620. $maxDepth = $comparator->getTarget() - 1;
  621. break;
  622. case '<=':
  623. $maxDepth = $comparator->getTarget();
  624. break;
  625. default:
  626. $minDepth = $maxDepth = $comparator->getTarget();
  627. }
  628. }
  629. $flags = \RecursiveDirectoryIterator::SKIP_DOTS;
  630. if ($this->followLinks) {
  631. $flags |= \RecursiveDirectoryIterator::FOLLOW_SYMLINKS;
  632. }
  633. $iterator = new Iterator\RecursiveDirectoryIterator($dir, $flags, $this->ignoreUnreadableDirs);
  634. if ($this->exclude) {
  635. $iterator = new Iterator\ExcludeDirectoryFilterIterator($iterator, $this->exclude);
  636. }
  637. $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::SELF_FIRST);
  638. if ($minDepth > 0 || $maxDepth < PHP_INT_MAX) {
  639. $iterator = new Iterator\DepthRangeFilterIterator($iterator, $minDepth, $maxDepth);
  640. }
  641. if ($this->mode) {
  642. $iterator = new Iterator\FileTypeFilterIterator($iterator, $this->mode);
  643. }
  644. if ($this->names || $this->notNames) {
  645. $iterator = new Iterator\FilenameFilterIterator($iterator, $this->names, $this->notNames);
  646. }
  647. if ($this->contains || $this->notContains) {
  648. $iterator = new Iterator\FilecontentFilterIterator($iterator, $this->contains, $this->notContains);
  649. }
  650. if ($this->sizes) {
  651. $iterator = new Iterator\SizeRangeFilterIterator($iterator, $this->sizes);
  652. }
  653. if ($this->dates) {
  654. $iterator = new Iterator\DateRangeFilterIterator($iterator, $this->dates);
  655. }
  656. if ($this->filters) {
  657. $iterator = new Iterator\CustomFilterIterator($iterator, $this->filters);
  658. }
  659. if ($this->paths || $this->notPaths) {
  660. $iterator = new Iterator\PathFilterIterator($iterator, $this->paths, $this->notPaths);
  661. }
  662. if ($this->sort || $this->reverseSorting) {
  663. $iteratorAggregate = new Iterator\SortableIterator($iterator, $this->sort, $this->reverseSorting);
  664. $iterator = $iteratorAggregate->getIterator();
  665. }
  666. return $iterator;
  667. }
  668. /**
  669. * Normalizes given directory names by removing trailing slashes.
  670. *
  671. * Excluding: (s)ftp:// wrapper
  672. *
  673. * @param string $dir
  674. *
  675. * @return string
  676. */
  677. private function normalizeDir($dir)
  678. {
  679. $dir = rtrim($dir, '/'.\DIRECTORY_SEPARATOR);
  680. if (preg_match('#^s?ftp://#', $dir)) {
  681. $dir .= '/';
  682. }
  683. return $dir;
  684. }
  685. }