PluginProbe
Packeta / trunk
Packeta vtrunk
2.3.2 2.3.1 trunk 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3.0 1.3.1 1.3.2 1.4 1.4.1 1.4.2 1.4.3 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 All 56 releases
packeta / deps / nette / finder / src / Utils / Finder.php

Finder.php in Packeta trunk, at deps/nette/finder/src/Utils/Finder.php

318 lines 10.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * This file is part of the Nette Framework (https://nette.org)
5 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6 */
7 declare (strict_types=1);
8 namespace Packetery\Nette\Utils;
9
10 use Packetery\Nette;
11 use RecursiveDirectoryIterator;
12 use RecursiveIteratorIterator;
13 /**
14 * Finder allows searching through directory trees using iterator.
15 *
16 * <code>
17 * Finder::findFiles('*.php')
18 * ->size('> 10kB')
19 * ->from('.')
20 * ->exclude('temp');
21 * </code>
22 *
23 * @implements \IteratorAggregate<string, \SplFileInfo>
24 */
25 class Finder implements \IteratorAggregate, \Countable
26 {
27 use \Packetery\Nette\SmartObject;
28 /** @var callable[] extension methods */
29 private static $extMethods = [];
30 /** @var array */
31 private $paths = [];
32 /** @var array of filters */
33 private $groups = [];
34 /** @var array filter for recursive traversing */
35 private $exclude = [];
36 /** @var int */
37 private $order = RecursiveIteratorIterator::SELF_FIRST;
38 /** @var int */
39 private $maxDepth = -1;
40 /** @var array */
41 private $cursor;
42 /**
43 * Begins search for files and directories matching mask.
44 * @param string ...$masks
45 * @return static
46 */
47 public static function find(...$masks) : self
48 {
49 $masks = \is_array($tmp = \reset($masks)) ? $tmp : $masks;
50 return (new static())->select($masks, 'isDir')->select($masks, 'isFile');
51 }
52 /**
53 * Begins search for files matching mask.
54 * @param string ...$masks
55 * @return static
56 */
57 public static function findFiles(...$masks) : self
58 {
59 $masks = \is_array($tmp = \reset($masks)) ? $tmp : $masks;
60 return (new static())->select($masks, 'isFile');
61 }
62 /**
63 * Begins search for directories matching mask.
64 * @param string ...$masks
65 * @return static
66 */
67 public static function findDirectories(...$masks) : self
68 {
69 $masks = \is_array($tmp = \reset($masks)) ? $tmp : $masks;
70 return (new static())->select($masks, 'isDir');
71 }
72 /**
73 * Creates filtering group by mask & type selector.
74 * @return static
75 */
76 private function select(array $masks, string $type) : self
77 {
78 $this->cursor =& $this->groups[];
79 $pattern = self::buildPattern($masks);
80 $this->filter(function (RecursiveDirectoryIterator $file) use($type, $pattern) : bool {
81 return !$file->isDot() && $file->{$type}() && (!$pattern || \preg_match($pattern, '/' . \strtr($file->getSubPathName(), '\\', '/')));
82 });
83 return $this;
84 }
85 /**
86 * Searches in the given folder(s).
87 * @param string ...$paths
88 * @return static
89 */
90 public function in(...$paths) : self
91 {
92 $this->maxDepth = 0;
93 return $this->from(...$paths);
94 }
95 /**
96 * Searches recursively from the given folder(s).
97 * @param string ...$paths
98 * @return static
99 */
100 public function from(...$paths) : self
101 {
102 if ($this->paths) {
103 throw new \Packetery\Nette\InvalidStateException('Directory to search has already been specified.');
104 }
105 $this->paths = \is_array($tmp = \reset($paths)) ? $tmp : $paths;
106 $this->cursor =& $this->exclude;
107 return $this;
108 }
109 /**
110 * Shows folder content prior to the folder.
111 * @return static
112 */
113 public function childFirst() : self
114 {
115 $this->order = RecursiveIteratorIterator::CHILD_FIRST;
116 return $this;
117 }
118 /**
119 * Converts Finder pattern to regular expression.
120 */
121 private static function buildPattern(array $masks) : ?string
122 {
123 $pattern = [];
124 foreach ($masks as $mask) {
125 $mask = \rtrim(\strtr($mask, '\\', '/'), '/');
126 $prefix = '';
127 if ($mask === '') {
128 continue;
129 } elseif ($mask === '*') {
130 return null;
131 } elseif ($mask[0] === '/') {
132 // absolute fixing
133 $mask = \ltrim($mask, '/');
134 $prefix = '(?<=^/)';
135 }
136 $pattern[] = $prefix . \strtr(\preg_quote($mask, '#'), ['\\*\\*' => '.*', '\\*' => '[^/]*', '\\?' => '[^/]', '\\[\\!' => '[^', '\\[' => '[', '\\]' => ']', '\\-' => '-']);
137 }
138 return $pattern ? '#/(' . \implode('|', $pattern) . ')$#Di' : null;
139 }
140 /********************* iterator generator ****************d*g**/
141 /** @deprecated */
142 public function count() : int
143 {
144 \trigger_error('\\Packetery\\Nette\\Utils\\Finder::count is deprecated.', \E_USER_DEPRECATED);
145 return \iterator_count($this->getIterator());
146 }
147 /**
148 * Returns iterator.
149 */
150 public function getIterator() : \Iterator
151 {
152 if (!$this->paths) {
153 throw new \Packetery\Nette\InvalidStateException('Call in() or from() to specify directory to search.');
154 } elseif (\count($this->paths) === 1) {
155 return $this->buildIterator((string) $this->paths[0]);
156 }
157 $iterator = new \AppendIterator();
158 foreach ($this->paths as $path) {
159 $iterator->append($this->buildIterator((string) $path));
160 }
161 return $iterator;
162 }
163 /**
164 * Returns per-path iterator.
165 */
166 private function buildIterator(string $path) : \Iterator
167 {
168 $iterator = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::FOLLOW_SYMLINKS);
169 if ($this->exclude) {
170 $iterator = new \RecursiveCallbackFilterIterator($iterator, function ($foo, $bar, RecursiveDirectoryIterator $file) : bool {
171 if (!$file->isDot() && !$file->isFile()) {
172 foreach ($this->exclude as $filter) {
173 if (!$filter($file)) {
174 return \false;
175 }
176 }
177 }
178 return \true;
179 });
180 }
181 if ($this->maxDepth !== 0) {
182 $iterator = new RecursiveIteratorIterator($iterator, $this->order);
183 $iterator->setMaxDepth($this->maxDepth);
184 }
185 $iterator = new \CallbackFilterIterator($iterator, function ($foo, $bar, \Iterator $file) : bool {
186 while ($file instanceof \OuterIterator) {
187 $file = $file->getInnerIterator();
188 }
189 foreach ($this->groups as $filters) {
190 foreach ($filters as $filter) {
191 if (!$filter($file)) {
192 continue 2;
193 }
194 }
195 return \true;
196 }
197 return \false;
198 });
199 return $iterator;
200 }
201 /********************* filtering ****************d*g**/
202 /**
203 * Restricts the search using mask.
204 * Excludes directories from recursive traversing.
205 * @param string ...$masks
206 * @return static
207 */
208 public function exclude(...$masks) : self
209 {
210 $masks = \is_array($tmp = \reset($masks)) ? $tmp : $masks;
211 $pattern = self::buildPattern($masks);
212 if ($pattern) {
213 $this->filter(function (RecursiveDirectoryIterator $file) use($pattern) : bool {
214 return !\preg_match($pattern, '/' . \strtr($file->getSubPathName(), '\\', '/'));
215 });
216 }
217 return $this;
218 }
219 /**
220 * Restricts the search using callback.
221 * @param callable(RecursiveDirectoryIterator): bool $callback
222 * @return static
223 */
224 public function filter(callable $callback) : self
225 {
226 $this->cursor[] = $callback;
227 return $this;
228 }
229 /**
230 * Limits recursion level.
231 * @return static
232 */
233 public function limitDepth(int $depth) : self
234 {
235 $this->maxDepth = $depth;
236 return $this;
237 }
238 /**
239 * Restricts the search by size.
240 * @param string $operator "[operator] [size] [unit]" example: >=10kB
241 * @return static
242 */
243 public function size(string $operator, ?int $size = null) : self
244 {
245 if (\func_num_args() === 1) {
246 // in $operator is predicate
247 if (!\preg_match('#^(?:([=<>!]=?|<>)\\s*)?((?:\\d*\\.)?\\d+)\\s*(K|M|G|)B?$#Di', $operator, $matches)) {
248 throw new \Packetery\Nette\InvalidArgumentException('Invalid size predicate format.');
249 }
250 [, $operator, $size, $unit] = $matches;
251 static $units = ['' => 1, 'k' => 1000.0, 'm' => 1000000.0, 'g' => 1000000000.0];
252 $size *= $units[\strtolower($unit)];
253 $operator = $operator ?: '=';
254 }
255 return $this->filter(function (RecursiveDirectoryIterator $file) use($operator, $size) : bool {
256 return self::compare($file->getSize(), $operator, $size);
257 });
258 }
259 /**
260 * Restricts the search by modified time.
261 * @param string $operator "[operator] [date]" example: >1978-01-23
262 * @param string|int|\DateTimeInterface $date
263 * @return static
264 */
265 public function date(string $operator, $date = null) : self
266 {
267 if (\func_num_args() === 1) {
268 // in $operator is predicate
269 if (!\preg_match('#^(?:([=<>!]=?|<>)\\s*)?(.+)$#Di', $operator, $matches)) {
270 throw new \Packetery\Nette\InvalidArgumentException('Invalid date predicate format.');
271 }
272 [, $operator, $date] = $matches;
273 $operator = $operator ?: '=';
274 }
275 $date = DateTime::from($date)->format('U');
276 return $this->filter(function (RecursiveDirectoryIterator $file) use($operator, $date) : bool {
277 return self::compare($file->getMTime(), $operator, $date);
278 });
279 }
280 /**
281 * Compares two values.
282 */
283 public static function compare($l, string $operator, $r) : bool
284 {
285 switch ($operator) {
286 case '>':
287 return $l > $r;
288 case '>=':
289 return $l >= $r;
290 case '<':
291 return $l < $r;
292 case '<=':
293 return $l <= $r;
294 case '=':
295 case '==':
296 return $l == $r;
297 case '!':
298 case '!=':
299 case '<>':
300 return $l != $r;
301 default:
302 throw new \Packetery\Nette\InvalidArgumentException("Unknown operator {$operator}.");
303 }
304 }
305 /********************* extension methods ****************d*g**/
306 /** @deprecated */
307 public function __call(string $name, array $args)
308 {
309 return isset(self::$extMethods[$name]) ? self::$extMethods[$name]($this, ...$args) : \Packetery\Nette\Utils\ObjectHelpers::strictCall(static::class, $name, \array_keys(self::$extMethods));
310 }
311 /** @deprecated */
312 public static function extensionMethod(string $name, callable $callback) : void
313 {
314 \trigger_error(__METHOD__ . '() is deprecated.', \E_USER_DEPRECATED);
315 self::$extMethods[$name] = $callback;
316 }
317 }
318