Skip to content

Commit

Permalink
[11.x] Add Customizable Date Validation Rule with Flexible Date Const…
Browse files Browse the repository at this point in the history
…raints (#53465)

* Add Flexible Date Validation Options: After, Before, and Custom Formats

* Add tests

* Formatting

* Renaming

* Fix separator

* Added between date

* Formatting

* Enabled seamless handling of `Carbon` and `DateTime` instances.

* Add tests

* Add `afterOrEqualToday` and `beforeOrEqualToday` and `betweenOrEqual`

* Added tests

* Formatting

* Fix typo

* formatting

---------

Co-authored-by: Taylor Otwell <[email protected]>
  • Loading branch information
michaelnabil230 and taylorotwell authored Jan 23, 2025
1 parent 94e6258 commit 1049c03
Show file tree
Hide file tree
Showing 3 changed files with 284 additions and 0 deletions.
11 changes: 11 additions & 0 deletions src/Illuminate/Validation/Rule.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Illuminate\Support\Traits\Macroable;
use Illuminate\Validation\Rules\ArrayRule;
use Illuminate\Validation\Rules\Can;
use Illuminate\Validation\Rules\Date;
use Illuminate\Validation\Rules\Dimensions;
use Illuminate\Validation\Rules\Email;
use Illuminate\Validation\Rules\Enum;
Expand Down Expand Up @@ -170,6 +171,16 @@ public static function prohibitedIf($callback)
return new ProhibitedIf($callback);
}

/**
* Get a date rule builder instance.
*
* @return \Illuminate\Validation\Rules\Date
*/
public static function date()
{
return new Date;
}

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

namespace Illuminate\Validation\Rules;

use DateTimeInterface;
use Illuminate\Support\Arr;
use Illuminate\Support\Traits\Conditionable;
use Illuminate\Support\Traits\Macroable;
use Stringable;

class Date implements Stringable
{
use Conditionable, Macroable;

/**
* The constraints for the date rule.
*/
protected array $constraints = ['date'];

/**
* Ensure the date has the given format.
*/
public function format(string $format): static
{
return $this->addRule('date_format:'.$format);
}

/**
* Ensure the date is before today.
*/
public function beforeToday(): static
{
return $this->before('today');
}

/**
* Ensure the date is after today.
*/
public function afterToday(): static
{
return $this->after('today');
}

/**
* Ensure the date is before or equal to today.
*/
public function todayOrBefore(): static
{
return $this->beforeOrEqual('today');
}

/**
* Ensure the date is after or equal to today.
*/
public function todayOrAfter(): static
{
return $this->afterOrEqual('today');
}

/**
* Ensure the date is before the given date or date field.
*/
public function before(DateTimeInterface|string $date): static
{
return $this->addRule('before:'.$this->formatDate($date));
}

/**
* Ensure the date is after the given date or date field.
*/
public function after(DateTimeInterface|string $date): static
{
return $this->addRule('after:'.$this->formatDate($date));
}

/**
* Ensure the date is on or before the specified date or date field.
*/
public function beforeOrEqual(DateTimeInterface|string $date): static
{
return $this->addRule('before_or_equal:'.$this->formatDate($date));
}

/**
* Ensure the date is on or after the given date or date field.
*/
public function afterOrEqual(DateTimeInterface|string $date): static
{
return $this->addRule('after_or_equal:'.$this->formatDate($date));
}

/**
* Ensure the date is between two dates or date fields.
*/
public function between(DateTimeInterface|string $from, DateTimeInterface|string $to): static
{
return $this->after($from)->before($to);
}

/**
* Ensure the date is between or equal to two dates or date fields.
*/
public function betweenOrEqual(DateTimeInterface|string $from, DateTimeInterface|string $to): static
{
return $this->afterOrEqual($from)->beforeOrEqual($to);
}

/**
* Add custom rules to the validation rules array.
*/
protected function addRule(array|string $rules): static
{
$this->constraints = array_merge($this->constraints, Arr::wrap($rules));

return $this;
}

/**
* Format the date for the validation rule.
*/
protected function formatDate(DateTimeInterface|string $date): string
{
return $date instanceof DateTimeInterface
? $date->format('Y-m-d')
: $date;
}

/**
* Convert the rule to a validation string.
*/
public function __toString(): string
{
return implode('|', $this->constraints);
}
}
138 changes: 138 additions & 0 deletions tests/Validation/ValidationDateRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?php

namespace Tests\Unit\Rules;

use Illuminate\Support\Carbon;
use Illuminate\Translation\ArrayLoader;
use Illuminate\Translation\Translator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Date;
use Illuminate\Validation\Validator;
use PHPUnit\Framework\TestCase;

class ValidationDateRuleTest extends TestCase
{
public function testDefaultDateRule()
{
$rule = Rule::date();
$this->assertEquals('date', (string) $rule);

$rule = new Date;
$this->assertSame('date', (string) $rule);
}

public function testDateFormatRule()
{
$rule = Rule::date()->format('d/m/Y');
$this->assertEquals('date|date_format:d/m/Y', (string) $rule);
}

public function testAfterTodayRule()
{
$rule = Rule::date()->afterToday();
$this->assertEquals('date|after:today', (string) $rule);

$rule = Rule::date()->todayOrAfter();
$this->assertEquals('date|after_or_equal:today', (string) $rule);
}

public function testBeforeTodayRule()
{
$rule = Rule::date()->beforeToday();
$this->assertEquals('date|before:today', (string) $rule);

$rule = Rule::date()->todayOrBefore();
$this->assertEquals('date|before_or_equal:today', (string) $rule);
}

public function testAfterSpecificDateRule()
{
$rule = Rule::date()->after(Carbon::parse('2024-01-01'));
$this->assertEquals('date|after:2024-01-01', (string) $rule);
}

public function testBeforeSpecificDateRule()
{
$rule = Rule::date()->before(Carbon::parse('2024-01-01'));
$this->assertEquals('date|before:2024-01-01', (string) $rule);
}

public function testAfterOrEqualSpecificDateRule()
{
$rule = Rule::date()->afterOrEqual(Carbon::parse('2024-01-01'));
$this->assertEquals('date|after_or_equal:2024-01-01', (string) $rule);
}

public function testBeforeOrEqualSpecificDateRule()
{
$rule = Rule::date()->beforeOrEqual(Carbon::parse('2024-01-01'));
$this->assertEquals('date|before_or_equal:2024-01-01', (string) $rule);
}

public function testBetweenDatesRule()
{
$rule = Rule::date()->between(Carbon::parse('2024-01-01'), Carbon::parse('2024-02-01'));
$this->assertEquals('date|after:2024-01-01|before:2024-02-01', (string) $rule);
}

public function testBetweenOrEqualDatesRule()
{
$rule = Rule::date()->betweenOrEqual('2024-01-01', '2024-02-01');
$this->assertEquals('date|after_or_equal:2024-01-01|before_or_equal:2024-02-01', (string) $rule);
}

public function testChainedRules()
{
$rule = Rule::date('Y-m-d H:i:s')
->format('Y-m-d')
->after('2024-01-01 00:00:00')
->before('2025-01-01 00:00:00');
$this->assertEquals('date|date_format:Y-m-d|after:2024-01-01 00:00:00|before:2025-01-01 00:00:00', (string) $rule);

$rule = Rule::date()
->format('Y-m-d')
->when(true, function ($rule) {
$rule->after('2024-01-01');
})
->unless(true, function ($rule) {
$rule->before('2025-01-01');
});
$this->assertSame('date|date_format:Y-m-d|after:2024-01-01', (string) $rule);
}

public function testDateValidation()
{
$trans = new Translator(new ArrayLoader, 'en');

$rule = Rule::date();

$validator = new Validator(
$trans,
['date' => 'not a date'],
['date' => $rule]
);

$this->assertSame(
$trans->get('validation.date'),
$validator->errors()->first('date')
);

$validator = new Validator(
$trans,
['date' => '2024-01-01'],
['date' => $rule]
);

$this->assertEmpty($validator->errors()->first('date'));

$rule = Rule::date()->between('2024-01-01', '2025-01-01');

$validator = new Validator(
$trans,
['date' => '2024-02-01'],
['date' => (string) $rule]
);

$this->assertEmpty($validator->errors()->first('date'));
}
}

0 comments on commit 1049c03

Please sign in to comment.