• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 12673986434

08 Jan 2025 03:42PM UTC coverage: 84.455% (+0.001%) from 84.454%
12673986434

Pull #9385

github

web-flow
Merge 06e47f0ee into e475fd8fa
Pull Request #9385: refactor: Fix phpstan expr.resultUnused

20699 of 24509 relevant lines covered (84.45%)

190.57 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.37
/system/Files/FileCollection.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Files;
15

16
use CodeIgniter\Exceptions\InvalidArgumentException;
17
use CodeIgniter\Files\Exceptions\FileException;
18
use CodeIgniter\Files\Exceptions\FileNotFoundException;
19
use Countable;
20
use Generator;
21
use IteratorAggregate;
22

23
/**
24
 * File Collection Class
25
 *
26
 * Representation for a group of files, with utilities for locating,
27
 * filtering, and ordering them.
28
 *
29
 * @template-implements IteratorAggregate<int, File>
30
 * @see \CodeIgniter\Files\FileCollectionTest
31
 */
32
class FileCollection implements Countable, IteratorAggregate
33
{
34
    /**
35
     * The current list of file paths.
36
     *
37
     * @var list<string>
38
     */
39
    protected $files = [];
40

41
    // --------------------------------------------------------------------
42
    // Support Methods
43
    // --------------------------------------------------------------------
44

45
    /**
46
     * Resolves a full path and verifies it is an actual directory.
47
     *
48
     * @throws FileException
49
     */
50
    final protected static function resolveDirectory(string $directory): string
51
    {
52
        if (! is_dir($directory = set_realpath($directory))) {
68✔
53
            $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1];
11✔
54

55
            throw FileException::forExpectedDirectory($caller['function']);
11✔
56
        }
57

58
        return $directory;
61✔
59
    }
60

61
    /**
62
     * Resolves a full path and verifies it is an actual file.
63
     *
64
     * @throws FileException
65
     */
66
    final protected static function resolveFile(string $file): string
67
    {
68
        if (! is_file($file = set_realpath($file))) {
65✔
69
            $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1];
4✔
70

71
            throw FileException::forExpectedFile($caller['function']);
4✔
72
        }
73

74
        return $file;
61✔
75
    }
76

77
    /**
78
     * Removes files that are not part of the given directory (recursive).
79
     *
80
     * @param list<string> $files
81
     *
82
     * @return list<string>
83
     */
84
    final protected static function filterFiles(array $files, string $directory): array
85
    {
86
        $directory = self::resolveDirectory($directory);
10✔
87

88
        return array_filter($files, static fn (string $value): bool => str_starts_with($value, $directory));
10✔
89
    }
90

91
    /**
92
     * Returns any files whose `basename` matches the given pattern.
93
     *
94
     * @param list<string> $files
95
     * @param string       $pattern Regex or pseudo-regex string
96
     *
97
     * @return list<string>
98
     */
99
    final protected static function matchFiles(array $files, string $pattern): array
100
    {
101
        // Convert pseudo-regex into their true form
102
        if (@preg_match($pattern, '') === false) {
24✔
103
            $pattern = str_replace(
21✔
104
                ['#', '.', '*', '?'],
21✔
105
                ['\#', '\.', '.*', '.'],
21✔
106
                $pattern
21✔
107
            );
21✔
108
            $pattern = "#\\A{$pattern}\\z#";
21✔
109
        }
110

111
        return array_filter($files, static fn ($value): bool => (bool) preg_match($pattern, basename($value)));
24✔
112
    }
113

114
    // --------------------------------------------------------------------
115
    // Class Core
116
    // --------------------------------------------------------------------
117

118
    /**
119
     * Loads the Filesystem helper and adds any initial files.
120
     *
121
     * @param list<string> $files
122
     */
123
    public function __construct(array $files = [])
124
    {
125
        helper(['filesystem']);
40✔
126

127
        $this->add($files)->define();
40✔
128
    }
129

130
    /**
131
     * Applies any initial inputs after the constructor.
132
     * This method is a stub to be implemented by child classes.
133
     */
134
    protected function define(): void
135
    {
136
    }
39✔
137

138
    /**
139
     * Optimizes and returns the current file list.
140
     *
141
     * @return list<string>
142
     */
143
    public function get(): array
144
    {
145
        $this->files = array_unique($this->files);
54✔
146
        sort($this->files, SORT_STRING);
54✔
147

148
        return $this->files;
54✔
149
    }
150

151
    /**
152
     * Sets the file list directly, files are still subject to verification.
153
     * This works as a "reset" method with [].
154
     *
155
     * @param list<string> $files The new file list to use
156
     *
157
     * @return $this
158
     */
159
    public function set(array $files)
160
    {
161
        $this->files = [];
3✔
162

163
        return $this->addFiles($files);
3✔
164
    }
165

166
    /**
167
     * Adds an array/single file or directory to the list.
168
     *
169
     * @param list<string>|string $paths
170
     *
171
     * @return $this
172
     */
173
    public function add($paths, bool $recursive = true)
174
    {
175
        $paths = (array) $paths;
52✔
176

177
        foreach ($paths as $path) {
52✔
178
            if (! is_string($path)) {
21✔
179
                throw new InvalidArgumentException('FileCollection paths must be strings.');
×
180
            }
181

182
            try {
183
                // Test for a directory
184
                self::resolveDirectory($path);
21✔
185
            } catch (FileException) {
9✔
186
                $this->addFile($path);
9✔
187

188
                continue;
9✔
189
            }
190

191
            $this->addDirectory($path, $recursive);
14✔
192
        }
193

194
        return $this;
52✔
195
    }
196

197
    // --------------------------------------------------------------------
198
    // File Handling
199
    // --------------------------------------------------------------------
200

201
    /**
202
     * Verifies and adds files to the list.
203
     *
204
     * @param list<string> $files
205
     *
206
     * @return $this
207
     */
208
    public function addFiles(array $files)
209
    {
210
        foreach ($files as $file) {
8✔
211
            $this->addFile($file);
7✔
212
        }
213

214
        return $this;
7✔
215
    }
216

217
    /**
218
     * Verifies and adds a single file to the file list.
219
     *
220
     * @return $this
221
     */
222
    public function addFile(string $file)
223
    {
224
        $this->files[] = self::resolveFile($file);
62✔
225

226
        return $this;
59✔
227
    }
228

229
    /**
230
     * Removes files from the list.
231
     *
232
     * @param list<string> $files
233
     *
234
     * @return $this
235
     */
236
    public function removeFiles(array $files)
237
    {
238
        $this->files = array_diff($this->files, $files);
11✔
239

240
        return $this;
11✔
241
    }
242

243
    /**
244
     * Removes a single file from the list.
245
     *
246
     * @return $this
247
     */
248
    public function removeFile(string $file)
249
    {
250
        return $this->removeFiles([$file]);
1✔
251
    }
252

253
    // --------------------------------------------------------------------
254
    // Directory Handling
255
    // --------------------------------------------------------------------
256

257
    /**
258
     * Verifies and adds files from each
259
     * directory to the list.
260
     *
261
     * @param list<string> $directories
262
     *
263
     * @return $this
264
     */
265
    public function addDirectories(array $directories, bool $recursive = false)
266
    {
267
        foreach ($directories as $directory) {
2✔
268
            $this->addDirectory($directory, $recursive);
2✔
269
        }
270

271
        return $this;
2✔
272
    }
273

274
    /**
275
     * Verifies and adds all files from a directory.
276
     *
277
     * @return $this
278
     */
279
    public function addDirectory(string $directory, bool $recursive = false)
280
    {
281
        $directory = self::resolveDirectory($directory);
34✔
282

283
        // Map the directory to depth 2 to so directories become arrays
284
        foreach (directory_map($directory, 2, true) as $key => $path) {
33✔
285
            if (is_string($path)) {
33✔
286
                $this->addFile($directory . $path);
33✔
287
            } elseif ($recursive && is_array($path)) {
27✔
288
                $this->addDirectory($directory . $key, true);
27✔
289
            }
290
        }
291

292
        return $this;
33✔
293
    }
294

295
    // --------------------------------------------------------------------
296
    // Filtering
297
    // --------------------------------------------------------------------
298

299
    /**
300
     * Removes any files from the list that match the supplied pattern
301
     * (within the optional scope).
302
     *
303
     * @param string      $pattern Regex or pseudo-regex string
304
     * @param string|null $scope   The directory to limit the scope
305
     *
306
     * @return $this
307
     */
308
    public function removePattern(string $pattern, ?string $scope = null)
309
    {
310
        if ($pattern === '') {
4✔
311
            return $this;
1✔
312
        }
313

314
        // Start with all files or those in scope
315
        $files = $scope === null ? $this->files : self::filterFiles($this->files, $scope);
3✔
316

317
        // Remove any files that match the pattern
318
        return $this->removeFiles(self::matchFiles($files, $pattern));
3✔
319
    }
320

321
    /**
322
     * Keeps only the files from the list that match
323
     * (within the optional scope).
324
     *
325
     * @param string      $pattern Regex or pseudo-regex string
326
     * @param string|null $scope   A directory to limit the scope
327
     *
328
     * @return $this
329
     */
330
    public function retainPattern(string $pattern, ?string $scope = null)
331
    {
332
        if ($pattern === '') {
4✔
333
            return $this;
1✔
334
        }
335

336
        // Start with all files or those in scope
337
        $files = $scope === null ? $this->files : self::filterFiles($this->files, $scope);
3✔
338

339
        // Matches the pattern within the scoped files and remove their inverse.
340
        return $this->removeFiles(array_diff($files, self::matchFiles($files, $pattern)));
3✔
341
    }
342

343
    /**
344
     * Keeps only the files from the list that match multiple patterns
345
     * (within the optional scope).
346
     *
347
     * @param list<string> $patterns Array of regex or pseudo-regex strings
348
     * @param string|null  $scope    A directory to limit the scope
349
     *
350
     * @return $this
351
     */
352
    public function retainMultiplePatterns(array $patterns, ?string $scope = null)
353
    {
354
        if ($patterns === []) {
5✔
355
            return $this;
1✔
356
        }
357

358
        if (count($patterns) === 1 && $patterns[0] === '') {
4✔
359
            return $this;
1✔
360
        }
361

362
        // Start with all files or those in scope
363
        $files = $scope === null ? $this->files : self::filterFiles($this->files, $scope);
3✔
364

365
        // Add files to retain to array
366
        $filesToRetain = [];
3✔
367

368
        foreach ($patterns as $pattern) {
3✔
369
            if ($pattern === '') {
3✔
370
                continue;
×
371
            }
372

373
            // Matches the pattern within the scoped files
374
            $filesToRetain = array_merge($filesToRetain, self::matchFiles($files, $pattern));
3✔
375
        }
376

377
        // Remove the inverse of files to retain
378
        return $this->removeFiles(array_diff($files, $filesToRetain));
3✔
379
    }
380

381
    // --------------------------------------------------------------------
382
    // Interface Methods
383
    // --------------------------------------------------------------------
384

385
    /**
386
     * Returns the current number of files in the collection.
387
     * Fulfills Countable.
388
     */
389
    public function count(): int
390
    {
391
        return count($this->files);
1✔
392
    }
393

394
    /**
395
     * Yields as an Iterator for the current files.
396
     * Fulfills IteratorAggregate.
397
     *
398
     * @return Generator<File>
399
     *
400
     * @throws FileNotFoundException
401
     */
402
    public function getIterator(): Generator
403
    {
404
        foreach ($this->get() as $file) {
1✔
405
            yield new File($file, true);
1✔
406
        }
407
    }
408
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc