Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[11.x] Add fluent Email validation rule #54067

Open
wants to merge 5 commits into
base: 11.x
Choose a base branch
from

Conversation

SanderMuller
Copy link
Contributor

This PR aims to provide a similar experience as the Password validation rule and File validation rule (#43271) for emails. It does so by providing a fluent, extendable Email rule object.

Basic usage

// Before
$request->validate([
  'email' => ['required', 'string', 'email', 'strict', 'dns'],
]);
 
// After
$request->validate([
  'email' => ['required', 'string', Rule::email()->strict()->dns()],
]);

Available methods

The following methods are available on the rule:

  • ::default: equivalent to the Password::default() rule
  • ::strictSecurity: equivalent to Rule::email()->strict()->dns()->spoof()
  • strict: equivalent to the strict rule
  • dns: equivalent to the dns rule
  • spoof: equivalent to the spoof rule
  • filter: equivalent to the filter rule
  • filterUnicode: equivalent to the filter_unicode rule
  • rfc: equivalent to the rfc rule

Extending for custom types

Whilst the File rule only ships with the ::image method as a custom type, the rule is Macroable, allowing each application to specify more granular file permissions as required.

// AppServiceProvider
public function boot()
{
  Email::macro('employee', function () {
    return Email::default()->rules('ends_with:@laravel.com');
  });
}

// Usage
$request->validate([
  'email' => ['required', Email::employee()->spoof()]
]);

Open for discussion

I've added strictSecurity method as an option to easily create a solid email validation check, since the differences between the available rules are rather complex. Not including this method would be a valid choice as well. However, I hope this method will help Laravel developers better secure the email inputs, for example example against spoofing. The composition of this methods (strict + DNS + spoof) is a result of running applications in production and handling all kinds of edge cases with invalid emails that passed validation before using this combination.

Thanks to @lukeraymonddowning for providing #43271. I've used his PR as inspiration for the body text of this PR.

Copy link

github-actions bot commented Jan 3, 2025

Thanks for submitting a PR!

Note that draft PR's are not reviewed. If you would like a review, please mark your pull request as ready for review in the GitHub user interface.

Pull requests that are abandoned in draft may be closed due to inactivity.

@@ -86,7 +86,7 @@ class File implements Rule, DataAwareRule, ValidatorAwareRule
* If no arguments are passed, the default file rule configuration will be returned.
*
* @param static|callable|null $callback
* @return static|null
* @return static|void
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

never returns null, but does return void

use Illuminate\Validation\Validator;
use PHPUnit\Framework\TestCase;

class ValidationEmailRuleTest extends TestCase
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test class inspired by ValidationFileRuleTest

use Stringable;
use function Illuminate\Support\enum_value;

class Email implements Rule, DataAwareRule, ValidatorAwareRule
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heavily inspired by src/Illuminate/Validation/Rules/File.php

@SanderMuller SanderMuller marked this pull request as ready for review January 3, 2025 11:58
@@ -124,7 +124,7 @@ public function __construct($min)
* If no arguments are passed, the default password rule configuration will be returned.
*
* @param static|callable|null $callback
* @return static|null
* @return static|void
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be better suited for a separate PR as this is not strongly related to the rest of this PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's an incorrect docblock in the two classes that are similar to this PR. This PR uses the correct return type and fixes it for the other usages. I think it's probably too small for a standalone PR, but will take it out and make a separate PR if the Laravel team prefers that

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I usually tiptoe around Taylor and something as small as this or a fly on his office wall can make him reject a PR, so I better don't take any chances.

@@ -86,7 +86,7 @@ class File implements Rule, DataAwareRule, ValidatorAwareRule
* If no arguments are passed, the default file rule configuration will be returned.
*
* @param static|callable|null $callback
* @return static|null
* @return static|void
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be better suited for a separate PR as this is not strongly related to the rest of this PR

src/Illuminate/Validation/Rules/Email.php Outdated Show resolved Hide resolved
src/Illuminate/Validation/Rules/Email.php Outdated Show resolved Hide resolved
src/Illuminate/Validation/Rules/Email.php Outdated Show resolved Hide resolved
@taylorotwell
Copy link
Member

While I like terse methods, I wonder if we should give these methods a bit more descriptive names? It's not super clear was spoof or dns means when chaining them together.

@taylorotwell taylorotwell marked this pull request as draft January 3, 2025 15:52
@SanderMuller
Copy link
Contributor Author

SanderMuller commented Jan 3, 2025

While I like terse methods, I wonder if we should give these methods a bit more descriptive names? It's not super clear was spoof or dns means when chaining them together.

@taylorotwell Thank you for reviewing the pull request and providing your feedback. I’ve updated the method names to make them more descriptive and aligned with Laravel’s validation conventions.

New names:

rfcCompliant(): Ensures compliance with RFC standards.
rfcCompliant(true): Adheres strictly to RFC without warnings (formerly strict).
nativeFilter(): Validates email format using FILTER_VALIDATE_EMAIL.
nativeFilter(true): Extends basic format validation to include Unicode support.
verifyMxRecord(): Verifies that the domain has a valid MX record.
preventEmailSpoofing(): Detects and prevents email spoofing.

I attempted to pick names aiming to balance clarity and brevity, ensuring developers can intuitively understand the intent of each rule when chaining them together. For example:

Rule::email()
    ->rfcCompliant(true)
    ->domainExists()
    ->preventEmailSpoofing();

Perhaps they are not perfect yet, but it might inspire you to make the final naming tweaks!

@SanderMuller SanderMuller marked this pull request as ready for review January 3, 2025 17:31
@SanderMuller
Copy link
Contributor Author

@taylorotwell If we’re moving away from string-based names, we could also consider @shaedrich’s suggestion of merging the rfcCompliant and rfcCompliantWithoutWarnings rules into a single rule, such as rfcCompliant(bool $strict). Similarly, combining basicFormat and basicFormatWithUnicode into basicFormat(bool $withUnicode) would simplify things, since these are essentially variants of each other.

This approach would reduce the number of methods from 6 to 4, making the API easier to use and more intuitive while maintaining clarity.

Let me know your thoughts!

@shaedrich
Copy link
Contributor

rfcCompliant: Ensures compliance with RFC standards.
rfcCompliantWithoutWarnings: Adheres strictly to RFC without warnings (formerly strict).

I would actually flip that negation around:
rfcCompliantWarningsAllowed: Ensures compliance with RFC standards.
strictRfcCompliance: Adheres strictly to RFC without warnings (formerly strict).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants