Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save vmosoti/8ef268588cbdbf33f263e4733571254b to your computer and use it in GitHub Desktop.
Save vmosoti/8ef268588cbdbf33f263e4733571254b to your computer and use it in GitHub Desktop.
custom validator in laravel to validate comma separated emails.
<?php
// custom validator in laravel to validate comma separated emails.
\Validator::extend("emails", function($attribute, $values, $parameters) {
$value = explode(',', $values);
$rules = [
'email' => 'required|email',
];
if ($value) {
foreach ($value as $email) {
$data = [
'email' => $email
];
$validator = \Validator::make($data, $rules);
if ($validator->fails()) {
return false;
}
}
return true;
}
});
// Custom message for that validation
// pass this array as third parameter in \Validator::make
array('emails' => ':attribute must have valid email addresses.');
// Usage:
$rules['notifications'] = 'emails'; // 'emails' is name of new rule.
@BmpCorp
Copy link

BmpCorp commented Dec 15, 2023

"attribute" => explode(',', $value)

can be further transformed to

"attribute" => array_map('trim', explode(',', $value))

if you want comma and space separated emails (which is more natural for some users) to be valid too.

@nickpoulos
Copy link

Here is a Laravel 11.x compatible version of a similar rule I wrote for my projects.

This rule supports custom delimiters (defaults to support comma, space, newlines), as well as minimum + maximum number of e-mails. It works properly with nested attributes.

I have also provided the Pest Unit tests which are fairly robust.

Hopefully this helps somebody else out.

<?php

namespace App\Website\Account\Rules;

use Closure;
use Illuminate\Support\Str;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Validator;
use Illuminate\Contracts\Validation\ValidationRule;

class DelimitedEmails implements ValidationRule
{
    protected array $delimiters;
    protected ?int $minEmails;
    protected ?int $maxEmails;

    /**
     * Constructor to accept optional custom delimiters and min/max email counts.
     *
     * @param array|string|null $delimiters  Delimiters to split emails by (default: comma, space, newline).
     * @param int|null $minEmails  Minimum number of emails allowed.
     * @param int|null $maxEmails  Maximum number of emails allowed.
     */
    public function __construct(array|string $delimiters = [',', ' ', "\n"], ?int $minEmails = null, ?int $maxEmails = null)
    {
        $this->delimiters = (array) $delimiters;
        $this->minEmails = $minEmails;
        $this->maxEmails = $maxEmails;
    }

    /**
     * Validate the email list.
     *
     * @param string $attribute
     * @param mixed $value
     * @param Closure $fail
     * @return void
     */
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $emails = array_values($this->splitEmails($value));

        if ($this->minEmails !== null && count($emails) < $this->minEmails) {
            $pluralMinimumEmailsLabel = Str::plural('email', $this->minEmails);
            $fail("The $attribute must contain at least {$this->minEmails} {$pluralMinimumEmailsLabel}.");
            return;
        }

        if ($this->maxEmails !== null && count($emails) > $this->maxEmails) {
            $pluralMaximumEmailsLabel = Str::plural('email', $this->maxEmails);
            $fail("The $attribute must contain no more than {$this->maxEmails} $pluralMaximumEmailsLabel.");
            return;
        }

        $data = [];

        Arr::set($data, $attribute, $emails);

        $validator = Validator::make(
            $data,
            ["$attribute.*" => 'required|email']
        );

        if (!$validator->passes()) {
            $fail("The $attribute must contain only valid email addresses.");
        }
    }

    /**
     * Split the email string by the provided delimiters.
     *
     * @param string $value
     * @return array
     */
    protected function splitEmails(string $value): array
    {
        $pattern = implode('|', array_map('preg_quote', $this->delimiters));
        return array_filter(preg_split("/($pattern)/", $value), fn($email) => !empty($email));
    }
}

Here are the Pest Unit tests:

<?php

use App\Website\Account\Rules\DelimitedEmails;
use Illuminate\Support\Facades\Validator;

it('validates a default comma-separated list of emails', function () {
    expect(Validator::make(
        ['emails' => '[email protected], [email protected], [email protected]'],
        ['emails' => [new DelimitedEmails()]]
    )->passes())->toBeTrue();
});

it('fails if invalid emails are present in default comma-separated list', function () {
    expect(Validator::make(
        ['emails' => '[email protected], invalid-email, [email protected]'],
        ['emails' => [new DelimitedEmails()]]
    )->passes())->toBeFalse();
});

it('validates with custom delimiters', function () {
    expect(Validator::make(
        ['emails' => '[email protected];[email protected]|[email protected]'],
        ['emails' => [new DelimitedEmails([';', '|'])]]
    )->passes())->toBeTrue();
});

it('fails when not enough emails are provided', function () {
    $validator = Validator::make(
        ['emails' => '[email protected]'],
        ['emails' => [new DelimitedEmails(',', 3)]]
    );
    expect($validator->passes())->toBeFalse();
    expect($validator->errors()->first('emails'))->toBe('The emails must contain at least 3 emails.');
});

it('fails when too many emails are provided', function () {
    $validator = Validator::make(
        ['emails' => '[email protected], [email protected], [email protected]'],
        ['emails' => [new DelimitedEmails(',', 1, 2)]]
    );
    expect($validator->passes())->toBeFalse();
    expect($validator->errors()->first('emails'))->toBe('The emails must contain no more than 2 emails.');
});

it('validates with newline-separated emails', function () {
    expect(Validator::make(
        ['emails' => "[email protected]\n[email protected]\n[email protected]"],
        ['emails' => [new DelimitedEmails("\n")]]
    )->passes())->toBeTrue();
});

it('validates with mixed delimiters', function () {
    expect(Validator::make(
        ['emails' => "[email protected] [email protected], [email protected]\n[email protected]"],
        ['emails' => [new DelimitedEmails([',', ' ', "\n"])]]
    )->passes())->toBeTrue();
});

dataset('invalidEmails', [
    ['[email protected], invalid-email'],
    ['invalid-email, invalid2@example'],
    ['[email protected];invalid-email;[email protected]'],
]);

it('fails when list contains invalid emails', function (string $emailList) {
    expect(Validator::make(
        ['emails' => $emailList],
        ['emails' => [new DelimitedEmails([',', ';'])]]
    )->passes())->toBeFalse();
})->with('invalidEmails');

it('validates the minimum and maximum number of emails', function () {
    expect(Validator::make(
        ['emails' => '[email protected], [email protected], [email protected]'],
        ['emails' => [new DelimitedEmails([',', ' '], 2, 4)]]
    )->passes())->toBeTrue();
});

it('validates emails from a nested attribute using dot notation', function () {
    expect(Validator::make(
        ['data' => ['user' => ['emails' => '[email protected], [email protected] [email protected]']]],
        ['data.user.emails' => [new DelimitedEmails([',', ' ', "\n"], 2, 5)]]
    )->passes())->toBeTrue();
});

it('fails validation for a nested attribute when emails are invalid', function () {
    $validator = Validator::make(
        ['data' => ['user' => ['emails' => '[email protected], invalid-email, [email protected]']]],
        ['data.user.emails' => [new DelimitedEmails([',', ' '])]]
    );
    expect($validator->passes())->toBeFalse();
    expect($validator->errors()->first('data.user.emails'))->toBe('The data.user.emails must contain only valid email addresses.');
});

it('fails validation for nested attribute when not enough emails are provided', function () {
    $validator = Validator::make(
        ['data' => ['user' => ['emails' => '[email protected]']]],
        ['data.user.emails' => [new DelimitedEmails(',', 3)]]
    );
    expect($validator->passes())->toBeFalse();
    expect($validator->errors()->first('data.user.emails'))->toBe('The data.user.emails must contain at least 3 emails.');
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment