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
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/Illuminate/Validation/Rule.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Illuminate\Validation\Rules\ArrayRule;
use Illuminate\Validation\Rules\Can;
use Illuminate\Validation\Rules\Dimensions;
use Illuminate\Validation\Rules\Email;
use Illuminate\Validation\Rules\Enum;
use Illuminate\Validation\Rules\ExcludeIf;
use Illuminate\Validation\Rules\Exists;
Expand Down Expand Up @@ -169,6 +170,16 @@ public static function prohibitedIf($callback)
return new ProhibitedIf($callback);
}

/**
* Get an email rule builder instance.
*
* @return \Illuminate\Validation\Rules\Email
*/
public static function email()
{
return new Email;
}

/**
* Get an enum rule builder instance.
*
Expand Down
331 changes: 331 additions & 0 deletions src/Illuminate/Validation/Rules/Email.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
<?php

namespace Illuminate\Validation\Rules;

use Egulias\EmailValidator\EmailValidator;
use Egulias\EmailValidator\Validation\DNSCheckValidation;
use Egulias\EmailValidator\Validation\Extra\SpoofCheckValidation;
use Egulias\EmailValidator\Validation\MultipleValidationWithAnd;
use Egulias\EmailValidator\Validation\NoRFCWarningsValidation;
use Egulias\EmailValidator\Validation\RFCValidation;
use Illuminate\Container\Container;
use Illuminate\Contracts\Validation\DataAwareRule;
use Illuminate\Contracts\Validation\Rule;
use Illuminate\Contracts\Validation\ValidatorAwareRule;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Traits\Conditionable;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Validation\Concerns\FilterEmailValidation;
use InvalidArgumentException;

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

{
use Conditionable, Macroable;

public bool $rfcCompliantStrict = false;

public bool $verifyMxRecord = false;

public bool $preventEmailSpoofing = false;

public bool $nativeFilter = false;

public bool $nativeFilterWithUnicodeAllowed = false;

public bool $rfcCompliant = false;

/**
* An array of custom rules that will be merged into the validation rules.
*
* @var array
*/
protected $customRules = [];

/**
* The error message after validation, if any.
*
* @var array
*/
protected $messages = [];

/**
* The data under validation.
*
* @var array
*/
protected $data;

/**
* The validator performing the validation.
*
* @var \Illuminate\Validation\Validator
*/
protected $validator;

/**
* The callback that will generate the "default" version of the file rule.
*
* @var string|array|callable|null
*/
public static $defaultCallback;

/**
* Set the default callback to be used for determining the email default rules.
*
* If no arguments are passed, the default email rule configuration will be returned.
*
* @param static|callable|null $callback
* @return static|void
*/
public static function defaults($callback = null)
{
if (is_null($callback)) {
return static::default();
}

if (! is_callable($callback) && ! $callback instanceof static) {
throw new InvalidArgumentException('The given callback should be callable or an instance of '.static::class);
}

static::$defaultCallback = $callback;
}

/**
* Get the default configuration of the file rule.
*
* @return static
*/
public static function default()
{
$email = is_callable(static::$defaultCallback)
? call_user_func(static::$defaultCallback)
: static::$defaultCallback;

return $email instanceof self ? $email : new self();
}

/**
* A strict rule set which includes DNS and Spoof checks.
*
* @return static
*/
public static function strictSecurity()
{
return (new self())
->rfcCompliant(true)
->verifyMxRecord()
->preventEmailSpoofing();
}

/**
* Validate against RFCValidation.
*
* @return $this
*/
public function rfcCompliant(bool $strict = false)
{
if ($strict) {
$this->rfcCompliantStrict = true;
} else {
$this->rfcCompliant = true;
}

return $this;
}

public function strict()
{
return $this->rfcCompliant(true);
}

/**
* Validate against DNSCheckValidation.
* Requires the PHP intl extension.
*
* @return $this
*/
public function verifyMxRecord()
{
$this->verifyMxRecord = true;

return $this;
}

/**
* Validate against SpoofCheckValidation.
* Requires the PHP intl extension.
*
* @return $this
*/
public function preventEmailSpoofing()
{
$this->preventEmailSpoofing = true;

return $this;
}

/**
* Validate against FilterEmailValidation.
*
* @return $this
*/
public function nativeFilter(bool $unicodeAllowed = false)
{
if ($unicodeAllowed) {
$this->nativeFilterWithUnicodeAllowed = true;
} else {
$this->nativeFilter = true;
}

return $this;
}

/**
* Specify additional validation rules that should be merged with the default rules during validation.
*
* @param string|array $rules
* @return $this
*/
public function rules($rules)
{
$this->customRules = array_merge($this->customRules, Arr::wrap($rules));

return $this;
}

/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
$this->messages = [];

if (! is_string($value) && ! (is_object($value) && method_exists($value, '__toString'))) {
return false;
}

$emailValidator = Container::getInstance()->make(EmailValidator::class);

$passes = $emailValidator->isValid((string) $value, new MultipleValidationWithAnd($this->buildValidationRules()));

if (! $passes) {
$this->messages = [trans('validation.email', ['attribute' => $attribute])];

return false;
}

if ($this->customRules) {
$validator = Validator::make(
$this->data,
[$attribute => $this->customRules],
$this->validator->customMessages,
$this->validator->customAttributes
);

if ($validator->fails()) {
return $this->fail($validator->messages()->all());
}
}

return true;
}

/**
* Build the array of underlying validation rules based on the current state.
*
* @return array
*/
protected function buildValidationRules()
{
$rules = [];

if ($this->rfcCompliant) {
$rules[] = new RFCValidation;
}

if ($this->rfcCompliantStrict) {
$rules[] = new NoRFCWarningsValidation;
}

if ($this->verifyMxRecord) {
$rules[] = new DNSCheckValidation;
}

if ($this->preventEmailSpoofing) {
$rules[] = new SpoofCheckValidation;
}

if ($this->nativeFilter) {
$rules[] = new FilterEmailValidation;
}

if ($this->nativeFilterWithUnicodeAllowed) {
$rules[] = FilterEmailValidation::unicode();
}

if ($rules) {
return $rules;
}

return [new RFCValidation];
}

/**
* Adds the given failures, and return false.
*
* @param array|string $messages
* @return bool
*/
protected function fail($messages)
{
$messages = Collection::wrap($messages)
->map(fn ($message) => $this->validator->getTranslator()->get($message))
->all();

$this->messages = array_merge($this->messages, $messages);

return false;
}

/**
* Get the validation error message.
*
* @return array
*/
public function message()
{
return $this->messages;
}

/**
* Set the current validator.
*
* @param \Illuminate\Contracts\Validation\Validator $validator
* @return $this
*/
public function setValidator($validator)
{
$this->validator = $validator;

return $this;
}

/**
* Set the current data under validation.
*
* @param array $data
* @return $this
*/
public function setData($data)
{
$this->data = $data;

return $this;
}
}
2 changes: 1 addition & 1 deletion src/Illuminate/Validation/Rules/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

*/
public static function defaults($callback = null)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Illuminate/Validation/Rules/Password.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.

*/
public static function defaults($callback = null)
{
Expand Down
Loading