Lines 83.33% 55 / 66
Methods 36.36% 4 / 11
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 __construct 100.00% 5 / 5 100.00% 1 / 1 1
 extension 80.00% 4 / 5 0.00% 0 / 1 3.07
 pattern 80.00% 4 / 5 0.00% 0 / 1 3.07
 recursive 100.00% 2 / 2 100.00% 1 / 1 1
 exclude 63.63% 7 / 11 0.00% 0 / 1 9.36
 ofType 75.00% 3 / 4 0.00% 0 / 1 4.25
 onlyFiles 100.00% 1 / 1 100.00% 1 / 1 1
 onlyFolders 0.00% 0 / 1 0.00% 0 / 1 2
 filesAndFolders 0.00% 0 / 1 0.00% 0 / 1 2
 collect 91.66% 22 / 24 0.00% 0 / 1 14.11
 matchesAnyPattern 100.00% 7 / 7 100.00% 1 / 1 5
18class FileSystemCollection
19{
20    /**
21     * Pattern type constants
22     */
23    const int PATTERN_REGEX = 0;
24    const int PATTERN_WILDCARD = 1;
25
26    /**
27     * Item type constants
28     */
29    const int TYPE_FILE = 0;
30    const int TYPE_FOLDER = 1;
31    const int TYPE_BOTH = -1;
32
33    /**
34     * The folder to search in
35     *
36     * @var Folder
37     */
38    private Folder $folder;
39
40    /**
41     * File extensions to filter by
42     *
43     * @var array
44     */
45    private array $extension;
46
47    /**
48     * Patterns to match filenames against
49     *
50     * @var array
51     */
52    private array $patterns;
53
54    /**
55     * Patterns to exclude filenames
56     *
57     * @var array
58     */
59    private array $exclude;
60
61    /**
62     * Whether to search recursively
63     *
64     * @var bool
65     */
66    private bool $recursive;
67
68    /**
69     * Type of items to collect (files, folders, or both)
70     *
71     * @var int
72     */
73    private int $itemType = -1;
74
75    /**
76     * Creates a new FileSystemCollection
77     *
78     * @param Folder $folder The folder to search in
79     */
80    public function __construct(Folder $folder)
81    {
82        $this->folder = $folder;
83        $this->extension = [];
84        $this->patterns = [];
85        $this->exclude = [];
86        $this->recursive = false;
87    }
88
89    /**
90     * Filters by file extension
91     *
92     * @param string|array $extension One or more extensions to filter by
93     * @return FileSystemCollection This collection for method chaining
94     */
95    public function extension(string|array $extension) : FileSystemCollection
96    {
97        if(is_string($extension)) {
98            $this->extension[] = strtolower($extension);
99        }
100        if(is_array($extension)) {
101            $this->extension = array_merge($this->extension, array_map('strtolower',$extension));
102        }
103
104        return $this;
105    }
106
107    /**
108     * Filters by filename pattern
109     *
110     * @param string|array $pattern One or more patterns to match against
111     * @param int $type The pattern type (PATTERN_REGEX or PATTERN_WILDCARD)
112     * @return FileSystemCollection This collection for method chaining
113     */
114    public function pattern(string|array $pattern, int $type = 0) : FileSystemCollection
115    {
116        if(is_string($pattern)) {
117            $this->patterns[$type][] = $pattern;
118        }
119        if(is_array($pattern)) {
120            $this->patterns[$type] = array_merge($this->patterns[$type], $pattern);
121        }
122        return $this;
123    }
124
125    /**
126     * Sets recursive search mode
127     *
128     * @return FileSystemCollection This collection for method chaining
129     */
130    public function recursive() : FileSystemCollection
131    {
132        $this->recursive = true;
133        return $this;
134    }
135
136    /**
137     * Excludes filenames matching the given pattern
138     *
139     * @param string|array $exclude Pattern(s) to exclude
140     * @param int $type The pattern type (PATTERN_REGEX or PATTERN_WILDCARD)
141     * @return FileSystemCollection This collection for method chaining
142     * @throws Exception If the pattern type is invalid
143     */
144    public function exclude(string|array $exclude, int $type = 0) : FileSystemCollection
145    {
146        if($type > 1 || $type < 0) {
147            throw new Exception("Invalid type provided, type must be PATTERN_REGEX or PATTERN_WILDCARD");
148        }
149
150        // Initialize the array if it doesn't exist
151        if (!isset($this->exclude[$type])) {
152            $this->exclude[$type] = [];
153        }
154
155        if(is_string($exclude)) {
156            $this->exclude[$type][] = $exclude;
157        }
158        if(is_array($exclude)) {
159            if (!isset($this->exclude[$type])) {
160                $this->exclude[$type] = [];
161            }
162            $this->exclude[$type] = array_merge($this->exclude[$type], $exclude);
163        }
164
165        return $this;
166    }
167
168    /**
169     * Specify the type of items to collect (files, folders, or both)
170     *
171     * @param int $type One of TYPE_FILE, TYPE_FOLDER, or TYPE_BOTH
172     * @return FileSystemCollection This collection for method chaining
173     * @throws Exception If the type is invalid
174     */
175    public function ofType(int $type) : FileSystemCollection
176    {
177        if ($type !== self::TYPE_FILE && $type !== self::TYPE_FOLDER && $type !== self::TYPE_BOTH) {
178            throw new Exception("Invalid type provided, type must be TYPE_FILE, TYPE_FOLDER, or TYPE_BOTH");
179        }
180
181        $this->itemType = $type;
182        return $this;
183    }
184
185    /**
186     * Shorthand method to collect only files
187     *
188     * @return FileSystemCollection This collection for method chaining
189     */
190    public function onlyFiles() : FileSystemCollection
191    {
192        return $this->ofType(self::TYPE_FILE);
193    }
194
195    /**
196     * Shorthand method to collect only folders
197     *
198     * @return FileSystemCollection This collection for method chaining
199     */
200    public function onlyFolders() : FileSystemCollection
201    {
202        return $this->ofType(self::TYPE_FOLDER);
203    }
204
205    /**
206     * Shorthand method to collect both files and folders
207     *
208     * @return FileSystemCollection This collection for method chaining
209     */
210    public function filesAndFolders() : FileSystemCollection
211    {
212        return $this->ofType(self::TYPE_BOTH);
213    }
214
215    /**
216     * Executes the search and returns the collection of matching items
217     *
218     * @return array Array of File and/or Folder objects matching the criteria
219     */
220    public function collect(): array
221    {
222        $items = [];
223
224        // Use FilesystemIterator for both recursive and non-recursive cases
225        if ($this->recursive) {
226            $iterator = new RecursiveIteratorIterator(
227                new RecursiveDirectoryIterator($this->folder->path, FilesystemIterator::SKIP_DOTS),
228                RecursiveIteratorIterator::SELF_FIRST
229            );
230        } else {
231            $iterator = new FilesystemIterator($this->folder->path, FilesystemIterator::SKIP_DOTS);
232        }
233
234        foreach ($iterator as $fileInfo) {
235            // Skip based on item type
236            if (($this->itemType === self::TYPE_FILE && !$fileInfo->isFile()) ||
237                ($this->itemType === self::TYPE_FOLDER && !$fileInfo->isDir())) {
238                continue;
239            }
240
241            // Handle folders
242            if ($fileInfo->isDir()) {
243                $items[] = new Folder($fileInfo->getRealPath(), true);
244                continue;
245            }
246
247            // For files, apply filtering
248            $filename = $fileInfo->getFilename();
249
250            // Skip if extension doesn't match
251            if (!empty($this->extension) &&
252                !in_array(strtolower(pathinfo($filename, PATHINFO_EXTENSION)), $this->extension)) {
253                continue;
254            }
255
256            // Skip if pattern matching is required but fails
257            if (!empty($this->patterns) && !$this->matchesAnyPattern($filename, $this->patterns)) {
258                continue;
259            }
260
261            // Skip if file matches any exclude pattern
262            if (!empty($this->exclude) && $this->matchesAnyPattern($filename, $this->exclude)) {
263                continue;
264            }
265
266            $items[] = new File($fileInfo->getRealPath(), "", true);
267        }
268
269        return $items;
270    }
271
272    /**
273     * Check if a filename matches any pattern in the given pattern sets
274     *
275     * @param string $filename The filename to check
276     * @param array $patternSets Array of patterns to check against
277     * @return bool True if the filename matches any pattern, false otherwise
278     */
279    private function matchesAnyPattern(string $filename, array $patternSets): bool
280    {
281        // Check regex patterns
282        if (isset($patternSets[self::PATTERN_REGEX])) {
283            if (array_any($patternSets[self::PATTERN_REGEX], fn($pattern) => preg_match($pattern, $filename))) {
284                return true;
285            }
286        }
287
288        // Check wildcard patterns
289        if (isset($patternSets[self::PATTERN_WILDCARD])) {
290            if (array_any($patternSets[self::PATTERN_WILDCARD], fn($pattern) => fnmatch($pattern, $filename))) {
291                return true;
292            }
293        }
294
295        return false;
296    }
297}