maize-tech/laravel-email-domain-rule
A Laravel validation rule that restricts email addresses by domain, letting you allow or block specific domains and disposable providers. Drop it into form requests or validators to keep signups clean with one simple, configurable rule.
This is a small, single-purpose package: it ships one validation rule that constrains an email address to (or away from) a list of domains. Release 3.2.0 is pure maintenance — PHP 8.3 support and Laravel 11.x compatibility — so there is no new API surface to learn.
composer require maize-tech/laravel-email-domain-rulesrc/Rules/EmailDomainRule.php first — it is the entire package. Read the constructor signature (allow / deny arrays) and the domain-extraction logic before you build on top of it, because that logic is the thing you will eventually need to extend.use Maize\EmailDomainRule\Rules\EmailDomainRule;
$request->validate([
'email' => [
'required',
'email',
new EmailDomainRule(deny: ['gmail.com', 'yahoo.com', 'hotmail.com']),
],
]);
If your Laravel/PHP floor is below PHP 8.2 / Laravel 10, pin to the previous 3.x line; 3.2.0 is aligned with the PHP 8.3 + Laravel 11 support window. There are no breaking changes in this release, so the upgrade is a safe composer update.
Prefer FormRequests over inline controller validation. The rule is stateful (it carries your domain lists), so keeping the lists next to the request class keeps them discoverable:
class StoreRegistrationRequest extends FormRequest
{
public function rules(): array
{
return [
'email' => ['required', 'email', new EmailDomainRule(allow: config('signup.allowed_domains'))],
];
}
}
Keep the lists in config, not in code. A config/signup.php with allowed_domains / blocked_domains lets ops change policy without a deploy of application code, and it keeps the same list reusable across registration, invite, and "change email" flows.
Allow-list vs deny-list are different products. Use allow for B2B/corporate-gated products (fails closed, new domains are blocked by default) and deny for consumer products filtering out free providers (fails open). Do not mix both unless you have thought through precedence — confirm which wins in the rule source before relying on it.
Reuse it beyond the request layer. The domain check is often needed outside validation — e.g. "is this existing user's domain now blocked?" — so extract the normalization/parse step into your own small helper and let both the validation rule and any backfill command share it. Otherwise you end up with two subtly different notions of "same domain".
Pair with the standard rules and order them. ['required', 'email', new EmailDomainRule(...)] — the email rule does the format work so the domain rule only ever sees something with an @.
Test with datasets. Because the rule is deterministic on input strings, Pest/PHPUnit datasets are the cheapest way to lock in policy: valid corporate domain, blocked free provider, subdomain, uppercase, IDN. A Validator::make(...)->passes() assertion is enough.
Multi-tenant / per-account allow-lists work fine — just build the rule inside rules() where you have access to the authenticated tenant, rather than as a static array on the class.
mail.example.com is not example.com. A plain in_array comparison will reject subdomains; decide explicitly whether you want exact match or suffix match, and write a test for it.Example.COM must be lowercased before comparison. Check whether the rule does this — if not, do it in your own extension.bücher.de may arrive as xn--bcher-kva.de after browser normalization. Domain lists authored in Unicode will silently never match.email fails first, the domain rule may still be invoked depending on bail; parsing a string with no @ is where naive implementations throw. Adding bail after required avoids odd error messages stacking up.php artisan config:cache in your deploy pipeline, or a list change will appear to do nothing in production.deny as a UX filter and keep real abuse handling (rate limiting, email verification) separate.Rule of your own that wraps it) and, if you want the string syntax email_domain:..., registering a custom validator extension via Validator::extend. Both are a few lines and easier to own than waiting on upstream.composer.json.How can I help you explore Laravel packages today?