Skip to content

Context Matching

Suspend includes these matchers out of the box:

MatcherTypeDescription
EmailMatcheremailEmail addresses with wildcard support
IpMatcheripIPv4/IPv6 addresses with CIDR support
PhoneMatcherphonePhone numbers (normalized)
DomainMatcherdomainDomain names with subdomain matching
CountryMatchercountryISO country codes
FingerprintMatcherfingerprintDevice/browser fingerprints
RegexMatcherregexRegular expression patterns
GlobMatcherglobShell-style glob patterns
ExactMatcherexactExact string matching
use Cline\Suspend\Facades\Suspend;
// Block specific email
Suspend::match('email', 'spammer@example.com')->suspend('Known spammer');
// Block entire domain (wildcard)
Suspend::match('email', '*@spam-domain.com')->suspend('Spam domain');
// Block pattern
Suspend::match('email', 'bot*@*')->suspend('Bot pattern');
// Block specific IP
Suspend::match('ip', '1.2.3.4')->suspend('Malicious IP');
// Block CIDR range
Suspend::match('ip', '192.168.1.0/24')->suspend('Internal network');
// Block IPv6
Suspend::match('ip', '2001:db8::/32')->suspend('IPv6 range');
// Block specific number (formatting is normalized)
Suspend::match('phone', '+1-555-123-4567')->suspend();
// Numbers are normalized, so these are equivalent:
// +15551234567, (555) 123-4567, 555.123.4567
// Block domain and all subdomains
Suspend::match('domain', 'malware.com')->suspend('Malware source');
// Matches: malware.com, www.malware.com, sub.malware.com
// Block by ISO country code
Suspend::match('country', 'XX')->suspend('Restricted country');

Simple wildcard matching without regex complexity:

// * matches any characters
Suspend::match('glob', 'spam@*')->suspend();
// ? matches single character
Suspend::match('glob', 'user?@example.com')->suspend();
// Character classes
Suspend::match('glob', 'user[123]@*')->suspend();
// Negated classes
Suspend::match('glob', 'test[!0-9]@*')->suspend();

For complex patterns:

// Regex pattern (include delimiters)
Suspend::match('regex', '/^bot\d+@/i')->suspend('Bot pattern');
// Match disposable email providers
Suspend::match('regex', '/@(tempmail|throwaway)\./i')
->suspend('Disposable email');
$suspended = Suspend::check()
->email('user@example.com')
->matches();

Check multiple contexts at once - returns true if ANY match:

$suspended = Suspend::check()
->email($request->input('email'))
->ip($request->ip())
->phone($request->input('phone'))
->domain($request->getHost())
->fingerprint($request->header('X-Device-Fingerprint'))
->matches();
$suspensions = Suspend::check()
->email($email)
->ip($ip)
->getSuspensions();
// Returns Collection of matching Suspension models
foreach ($suspensions as $suspension) {
echo "Blocked by: {$suspension->reason}";
}

Automatically extract context from the current request:

$suspended = Suspend::check()
->fromRequest($request)
->matches();

Matchers validate patterns before creating suspensions:

// This will fail - invalid email pattern
Suspend::match('email', 'not-an-email')->suspend();
// This will fail - invalid CIDR
Suspend::match('ip', '192.168.1.0/33')->suspend();
// This will fail - invalid regex
Suspend::match('regex', '/unclosed[/')->suspend();

Create your own matcher for custom context types:

use Cline\Suspend\Matchers\Contracts\Matcher;
class UsernamePatternMatcher implements Matcher
{
public function type(): string
{
return 'username';
}
public function normalize(mixed $value): string
{
return mb_strtolower(trim((string) $value));
}
public function matches(string $pattern, mixed $value): bool
{
return fnmatch($pattern, $this->normalize($value));
}
public function validate(mixed $value): bool
{
$normalized = $this->normalize($value);
return $normalized !== '' && strlen($normalized) <= 255;
}
public function extract(mixed $value): ?string
{
return null; // No extraction for usernames
}
}

Register your custom matcher:

// In a service provider
use Cline\Suspend\Facades\Suspend;
public function boot()
{
Suspend::registerMatcher(new UsernamePatternMatcher());
}

Use it:

Suspend::match('username', 'bot_*')->suspend('Bot username pattern');
Suspend::check()->add('username', $username)->matches();
$disposableProviders = [
'*@tempmail.com',
'*@throwaway.io',
'*@10minutemail.com',
'*@guerrillamail.com',
];
foreach ($disposableProviders as $pattern) {
Suspend::match('email', $pattern)->suspend('Disposable email provider');
}
$badRanges = [
'192.0.2.0/24', // TEST-NET-1
'198.51.100.0/24', // TEST-NET-2
'203.0.113.0/24', // TEST-NET-3
];
foreach ($badRanges as $range) {
Suspend::match('ip', $range)->suspend('Reserved/test IP range');
}
$restrictedCountries = ['XX', 'YY', 'ZZ'];
foreach ($restrictedCountries as $country) {
Suspend::match('country', $country)
->suspend('Service not available in this region');
}
// Block suspicious email patterns
Suspend::match('regex', '/^[a-z]{20,}@/i')
->suspend('Suspicious random email');
// Block VoIP phone prefixes
Suspend::match('phone', '+1800*')->suspend('Toll-free number');
Suspend::match('phone', '+1888*')->suspend('Toll-free number');