Lines
100.00%
19 / 19
Methods
100.00%
3 / 3
Classes
100.00%
1 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| __construct | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 5 | ||
| apply | 100.00% | 11 / 11 | 100.00% | 1 / 1 | 6 | ||
| matches | 100.00% | 3 / 3 | 100.00% | 1 / 1 | 3 | ||
| 20 | final class ModelFilter | |
| 21 | { | |
| 22 | private readonly string|null $filter; | |
| 23 | private readonly string|null $exclude; | |
| 24 | ||
| 25 | /** | |
| 26 | * @param string|null $filter Include pattern (null = no filter) | |
| 27 | * @param string|null $exclude Exclude pattern (null = no filter) | |
| 28 | * @throws \InvalidArgumentException When a pattern is empty | |
| 29 | */ | |
| 30 | public function __construct(string|null $filter, string|null $exclude) | |
| 31 | { | |
| 32 | $this->filter = $filter === null ? null : trim($filter, '\'"'); | |
| 33 | $this->exclude = $exclude === null ? null : trim($exclude, '\'"'); | |
| 34 | ||
| 35 | foreach (['filter' => $this->filter, 'exclude' => $this->exclude] as $name => $value) { | |
| 36 | if ($value === '') { | |
| 37 | throw new \InvalidArgumentException("Invalid {$name}: value must not be empty."); | |
| 38 | } | |
| 39 | } | |
| 40 | } | |
| 41 | ||
| 42 | /** | |
| 43 | * Split models into active and excluded buckets (exclude wins). | |
| 44 | * | |
| 45 | * @param list<class-string> $models | |
| 46 | * @return array{active: list<class-string>, excluded: list<class-string>} | |
| 47 | */ | |
| 48 | public function apply(array $models): array | |
| 49 | { | |
| 50 | $active = []; | |
| 51 | $excluded = []; | |
| 52 | ||
| 53 | foreach ($models as $model) { | |
| 54 | if ($this->exclude !== null && self::matches($model, $this->exclude)) { | |
| 55 | $excluded[] = $model; | |
| 56 | continue; | |
| 57 | } | |
| 58 | ||
| 59 | if ($this->filter !== null && !self::matches($model, $this->filter)) { | |
| 60 | $excluded[] = $model; | |
| 61 | continue; | |
| 62 | } | |
| 63 | ||
| 64 | $active[] = $model; | |
| 65 | } | |
| 66 | ||
| 67 | return ['active' => $active, 'excluded' => $excluded]; | |
| 68 | } | |
| 69 | ||
| 70 | /** | |
| 71 | * Match a class name against a filter pattern. | |
| 72 | * | |
| 73 | * A pattern that starts with an alphanumeric character, or that is not | |
| 74 | * a valid regular expression, is treated as a literal substring match | |
| 75 | * (case-insensitive) — so backslashes in fully-qualified class names | |
| 76 | * (e.g. App\Models\Rsvp) are treated literally rather than as invalid | |
| 77 | * regex escapes. Any other pattern is used verbatim as a regex. | |
| 78 | */ | |
| 79 | public static function matches(string $class, string $pattern): bool | |
| 80 | { | |
| 81 | if (preg_match('/[a-zA-Z0-9]/', substr($pattern, 0, 1)) === 1 || @preg_match($pattern, '') === false) { | |
| 82 | // Literal substring mode. | |
| 83 | $pattern = sprintf('{%s}i', preg_quote($pattern, '/')); | |
| 84 | } | |
| 85 | ||
| 86 | return @preg_match($pattern, $class) === 1; | |
| 87 | } | |
| 88 | } |