<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
/**
* Finder allows searching through directory trees using iterator.
*
* Finder::findFiles('*.php')
* ->size('> 10kB')
* ->from('.')
* ->exclude('temp');
*
* @implements \IteratorAggregate<string, FileInfo>
*/
class Finder implements \IteratorAggregate
{
use Nette\SmartObject;
/** @var array<array{string, string}> */
private array $find = [];
/** @var string[] */
private array $in = [];
/** @var \Closure[] */
private array $filters = [];
/** @var \Closure[] */
private array $descentFilters = [];
/** @var array<string|self> */
private array $appends = [];
private bool $childFirst = false;
/** @var ?callable */
private $sort;
private int $maxDepth = -1;
private bool $ignoreUnreadableDirs = true;
/**
* Begins search for files and directories matching mask.
*/
public static function find(string|array $masks = ['*']): static
{
$masks = is_array($masks) ? $masks : func_get_args(); // compatibility with variadic
return (new static)->addMask($masks, 'dir')->addMask($masks, 'file');
}
/**
* Begins search for files matching mask.
*/
public static function findFiles(string|array $masks = ['*']): static
{
$masks = is_array($masks) ? $masks : func_get_args(); // compatibility with variadic
return (new static)->addMask($masks, 'file');
}
/**
* Begins search for directories matching mask.
*/
public static function findDirectories(string|array $masks = ['*']): static
{
$masks = is_array($masks) ? $masks : func_get_args(); // compatibility with variadic
return (new static)->addMask($masks, 'dir');
}
/**
* Finds files matching the specified masks.
*/
public function files(string|array $masks = ['*']): static
{
return $this->addMask((array) $masks, 'file');
}
/**
* Finds directories matching the specified masks.
*/
public function directories(string|array $masks = ['*']): static
{
return $this->addMask((array) $masks, 'dir');
}
private function addMask(array $masks, string $mode): static
{
foreach ($masks as $mask) {
$mask = FileSystem::unixSlashes($mask);
if ($mode === 'dir') {
$mask = rtrim($mask, '/');
}
if ($mask === '' || ($mode === 'file' && str_ends_with($mask, '/'))) {
throw new Nette\InvalidArgumentException("Invalid mask '$mask'");
}
if (str_starts_with($mask, '**/')) {
$mask = substr($mask, 3);
}
$this->find[] = [$mask, $mode];
}
return $this;
}
/**
* Searches in the given directories. Wildcards are allowed.
*/
public function in(string|array $paths): static
{
$paths = is_array($paths) ? $paths : func_get_args(); // compatibility with variadic
$this->addLocation($paths, '');
return $this;
}
/**
* Searches recursively from the given directories. Wildcards are allowed.
*/
public function from(string|array $paths): static
{
$paths = is_array($paths) ? $paths : func_get_args(); // compatibility with variadic
$this->addLocation($paths, '/**');
return $this;
}
private function addLocation(array $paths, string $ext): void
{
foreach ($paths as $path) {
if ($path === '') {
throw new Nette\InvalidArgumentException("Invalid directory '$path'");
}
$path = rtrim(FileSystem::unixSlashes($path), '/');
$this->in[] = $path . $ext;
}
}
/**
* Lists directory's contents before the directory itself. By default, this is disabled.
*/
public function childFirst(bool $state = true): static
{
$this->childFirst = $state;
return $this;
}
/**
* Ignores unreadable directories. By default, this is enabled.
*/
public function ignoreUnreadableDirs(bool $state = true): static
{
$this->ignoreUnreadableDirs = $state;
return $this;
}
/**
* Set a compare function for sorting directory entries. The function will be called to sort entries from the same directory.
* @param callable(FileInfo, FileInfo): int $callback
*/
public function sortBy(callable $callback): static
{
$this->sort = $callback;
return $this;
}
/**
* Sorts files in each directory naturally by name.
*/
public function sortByName(): static
{
$this->sort = fn(FileInfo $a, FileInfo $b): int => strnatcmp($a->getBasename(), $b->getBasename());
return $this;
}
/**
* Adds the specified paths or appends a new finder that returns.
*/
public function append(string|array|null $paths = null): static
{
if ($paths === null) {
return $this->appends[] = new static;
}
$this->appends = array_merge($this->appends, (array) $paths);
return $this;
}
/********************* filtering ****************d*g**/
/**
* Skips entries that matches the given masks relative to the ones defined with the in() or from() methods.
*/
public function exclude(string|array $masks): static
{
$masks = is_array($masks) ? $masks : func_get_args(); // compatibility with variadic
foreach ($masks as $mask) {
$mask = FileSystem::unixSlashes($mask);
if (!preg_match('~^/?(\*\*/)?(.+)(/\*\*|/\*|/|)$~D', $mask, $m)) {
throw new Nette\InvalidArgumentException("Invalid mask '$mask'");
}
$end = $m[3];
$re = $this->buildPattern($m[2]);
$filter = fn(FileInfo $file): bool => ($end && !$file->isDir())
|| !preg_match($re, FileSystem::unixSlashes($file->getRelativePathname()));
$this->descentFilter($filter);
if ($end !== '/*') {
$this->filter($filter);
}
}
return $this;
}
/**
* Yields only entries which satisfy the given filter.
* @param callable(FileInfo): bool $callback
*/
public function filter(callable $callback): static
{
$this->filters[] = \Closure::fromCallable($callback);
return $this;
}
/**
* It descends only to directories that match the specified filter.
* @param callable(FileInfo): bool $callback
*/
public function descentFilter(callable $callback): static
{
$this->descentFilters[] = \Closure::fromCallable($callback);
return $this;
}
/**
* Sets the maximum depth of entries.
*/
public function limitDepth(?int $depth): static
{
$this->maxDepth = $depth ?? -1;
return $this;
}
/**
* Restricts the search by size. $operator accepts "[operator] [size] [unit]" example: >=10kB
*/
public function size(string $operator, ?int $size = null): static
{
if (func_num_args() === 1) { // in $operator is predicate
if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?((?:\d*\.)?\d+)\s*(K|M|G|)B?$#Di', $operator, $matches)) {
throw new Nette\InvalidArgumentException('Invalid size predicate format.');
}
[, $operator, $size, $unit] = $matches;
$units = ['' => 1, 'k' => 1e3, 'm' => 1e6, 'g' => 1e9];
$size *= $units[strtolower($unit)];
$operator = $operator ?: '=';
}
return $this->filter(fn(FileInfo $file): bool => !$file->isFile() || Helpers::compare($file->getSize(), $operator, $size));
}
/**
* Restricts the search by modified time. $operator accepts "[operator] [date]" example: >1978-01-23
*/
public function date(string $operator, string|int|\DateTimeInterface|null $date = null): static
{
if (func_num_args() === 1) { // in $operator is predicate
if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?(.+)$#Di', $operator, $matches)) {
throw new Nette\InvalidArgumentException('Invalid date predicate format.');
}
[, $operator, $date] = $matches;
$operator = $operator ?: '=';
}
$date = DateTime::from($date)->format('U');
return $this->filter(fn(FileInfo $file): bool => !$file->isFile() || Helpers::compare($file->getMTime(), $operator, $date));
}
/********************* iterator generator ****************d*g**/
/**
* Returns an array with all found files and directories.
* @return list<FileInfo>
*/
public function collect(): array
{
return iterator_to_array($this->getIterator(), preserve_keys: false);
}
/** @return \Generator<string, FileInfo> */
public function getIterator(): \Generator
{
$plan = $this->buildPlan();
foreach ($plan as $dir => $searches) {
yield from $this->traverseDir($dir, $searches);
}
foreach ($this->appends as $item) {
if ($item instanceof self) {
yield from $item->getIterator();
} else {
$item = FileSystem::platformSlashes($item);
yield $item => new FileInfo($item);
}
}
}
/**
* @param array<object{pattern: string, mode: string, recursive: bool}> $searches
* @param string[] $subdirs
* @return \Generator<string, FileInfo>
*/
private function traverseDir(string $dir, array $searches, array $subdirs = []): \Generator
{
if ($this->maxDepth >= 0 && count($subdirs) > $this->maxDepth) {
return;
} elseif (!is_dir($dir)) {
throw new Nette\InvalidStateException(sprintf("Directory '%s' does not exist.", rtrim($dir, '/\\')));
}
try {
$pathNames = new \FilesystemIterator($dir, \FilesystemIterator::FOLLOW_SYMLINKS | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::UNIX_PATHS);
} catch (\UnexpectedValueException $e) {
if ($this->ignoreUnreadableDirs) {
return;
} else {
throw new Nette\InvalidStateException($e->getMessage());
}
}
$files = $this->convertToFiles($pathNames, implode('/', $subdirs), FileSystem::isAbsolute($dir));
if ($this->sort) {
$files = iterator_to_array($files);
usort($files, $this->sort);
}
foreach ($files as $file) {
$pathName = $file->getPathname();
$cache = $subSearch = [];
if ($file->isDir()) {
foreach ($searches as $search) {
if ($search->recursive && $this->proveFilters($this->descentFilters, $file, $cache)) {
$subSearch[] = $search;
}
}
}
if ($this->childFirst && $subSearch) {
yield from $this->traverseDir($pathName, $subSearch, array_merge($subdirs, [$file->getBasename()]));
}
$relativePathname = FileSystem::unixSlashes($file->getRelativePathname());
foreach ($searches as $search) {
if (
$file->{'is' . $search->mode}()
&& preg_match($search->pattern, $relativePathname)
&& $this->proveFilters($this->filters, $file, $cache)
) {
yield $pathName => $file;
break;
}
}
if (!$this->childFirst && $subSearch) {
yield from $this->traverseDir($pathName, $subSearch, array_merge($subdirs, [$file->getBasename()]));
}
}
}
private function convertToFiles(iterable $pathNames, string $relativePath, bool $absolute): \Generator
{
foreach ($pathNames as $pathName) {
if (!$absolute) {
$pathName = preg_replace('~\.?/~A', '', $pathName);
}
$pathName = FileSystem::platformSlashes($pathName);
yield new FileInfo($pathName, $relativePath);
}
}
private function proveFilters(array $filters, FileInfo $file, array &$cache): bool
{
foreach ($filters as $filter) {
$res = &$cache[spl_object_id($filter)];
$res ??= $filter($file);
if (!$res) {
return false;
}
}
return true;
}
/** @return array<string, array<object{pattern: string, mode: string, recursive: bool}>> */
private function buildPlan(): array
{
$plan = $dirCache = [];
foreach ($this->find as [$mask, $mode]) {
$splits = [];
if (FileSystem::isAbsolute($mask)) {
if ($this->in) {
throw new Nette\InvalidStateException("You cannot combine the absolute path in the mask '$mask' and the directory to search '{$this->in[0]}'.");
}
$splits[] = self::splitRecursivePart($mask);
} else {
foreach ($this->in ?: ['.'] as $in) {
$in = strtr($in, ['[' => '[[]', ']' => '[]]']); // in path, do not treat [ and ] as a pattern by glob()
$splits[] = self::splitRecursivePart($in . '/' . $mask);
}
}
foreach ($splits as [$base, $rest, $recursive]) {
$base = $base === '' ? '.' : $base;
$dirs = $dirCache[$base] ??= strpbrk($base, '*?[')
? glob($base, GLOB_NOSORT | GLOB_ONLYDIR | GLOB_NOESCAPE)
: [strtr($base, ['[[]' => '[', '[]]' => ']'])]; // unescape [ and ]
if (!$dirs) {
throw new Nette\InvalidStateException(sprintf("Directory '%s' does not exist.", rtrim($base, '/\\')));
}
$search = (object) ['pattern' => $this->buildPattern($rest), 'mode' => $mode, 'recursive' => $recursive];
foreach ($dirs as $dir) {
$plan[$dir][] = $search;
}
}
}
return $plan;
}
/**
* Since glob() does not know ** wildcard, we divide the path into a part for glob and a part for manual traversal.
*/
private static function splitRecursivePart(string $path): array
{
$a = strrpos($path, '/');
$parts = preg_split('~(?<=^|/)\*\*($|/)~', substr($path, 0, $a + 1), 2);
return isset($parts[1])
? [$parts[0], $parts[1] . substr($path, $a + 1), true]
: [$parts[0], substr($path, $a + 1), false];
}
/**
* Converts wildcards to regular expression.
*/
private function buildPattern(string $mask): string
{
if ($mask === '*') {
return '##';
} elseif (str_starts_with($mask, './')) {
$anchor = '^';
$mask = substr($mask, 2);
} else {
$anchor = '(?:^|/)';
}
$pattern = strtr(
preg_quote($mask, '#'),
[
'\*\*/' => '(.+/)?',
'\*' => '[^/]*',
'\?' => '[^/]',
'\[\!' => '[^',
'\[' => '[',
'\]' => ']',
'\-' => '-',
],
);
return '#' . $anchor . $pattern . '$#D' . (defined('PHP_WINDOWS_VERSION_BUILD') ? 'i' : '');
}
}
Service Section
Discover how our services are designed to enhance your NFC Pay experience with convenience, security, and innovative solutions. From managing transactions to secure payments, we are dedicated to providing seamless support every step of the way.
Easily save your credit and debit card details within our app for quick and secure transactions. This feature ensures that your payment information is protected with advanced encryption and can be used for future purchases with just a tap.
Transfer funds quickly and securely between users with our streamlined money transfer service. Simply select the recipient, enter the amount, and authorize the transaction for instant, hassle-free transfers.
Activate your merchant account effortlessly to start receiving payments. Our intuitive setup process ensures that you can begin accepting transactions smoothly, helping your business thrive with minimal setup time.
Keep track of all your transactions in real time through our app. Monitor payment statuses, view transaction history, and manage your account efficiently, ensuring complete control over your financial activities.
Our dedicated support team is available to assist you with any queries or issues. Whether you need help with setting up your account or resolving transaction-related questions, we’re here to provide prompt and reliable assistance.