Валидация дат и времени в Laravel
Даты приходят в формах как строки, и поле “дата рождения” со значением abc123 или 31.02.2025 без валидации попадёт в базу как есть. Laravel предоставляет набор правил для проверки дат, временных диапазонов и часовых поясов.
Если вы только знакомитесь с валидацией в Laravel, начните с основ. Для проверки строк, чисел и email - строки и числа.
Правило date
Правило date проверяет, что значение является валидной, нерелятивной датой через PHP-функцию strtotime(). Принимает большинство распространённых форматов:
$request->validate([
'birthday' => ['required', 'date'],
'hired_at' => ['nullable', 'date'],
]);
date пропустит 2025-03-15, March 15, 2025, 15.03.2025 - всё, что strtotime сумеет разобрать как абсолютную дату. Относительные выражения вроде next Monday или +3 days будут отвергнуты, хотя strtotime сам по себе их понимает.
В этой гибкости кроется проблема: если нужен конкретный формат ввода, date пропустит слишком многое. Для строгого формата используйте date_format.
Отдельное правило date_equals проверяет совпадение с конкретной датой:
$request->validate([
'confirmation' => ['required', 'date', 'date_equals:2025-12-31'],
]);
Используется редко, но бывает нужно для подтверждения фиксированного дедлайна.
Формат даты: date_format
Когда важен точный формат ввода, используйте date_format вместо date. Правило принимает один или несколько форматов через запятую:
$request->validate([
'birthday' => ['required', 'date_format:Y-m-d'],
'event_date' => ['required', 'date_format:d.m.Y'],
'flexible' => ['required', 'date_format:Y-m-d,d/m/Y'],
]);
Форматы соответствуют PHP DateTime::createFromFormat(). Частые варианты:
Y-m-d-2025-03-15(ISO, рекомендуется для API)d.m.Y-15.03.2025(принят в России и Европе)d/m/Y-15/03/2025m/d/Y-03/15/2025(формат США)Y-m-d H:i:s-2025-03-15 14:30:00(datetime)Y-m-d\TH:i:sP- ISO 8601 с таймзоной
Важно: date и date_format нельзя использовать вместе для одного поля. date опирается на strtotime, а date_format на DateTime::createFromFormat - логика парсинга у них разная, и результаты будут непредсказуемыми.
Fluent date builder
Laravel предоставляет Rule::date() - fluent-построитель правил для дат:
use Illuminate\Validation\Rule;
$request->validate([
'start_date' => [
'required',
Rule::date()->format('Y-m-d'),
],
]);
Это эквивалент date_format:Y-m-d, но читается чище в цепочке с другими условиями. Основное преимущество builder проявляется, когда нужно комбинировать формат и диапазон - вместо нескольких строковых правил получается одна цепочка вызовов.
Диапазоны: after и before
Правила after и before сравнивают дату с заданной границей. Значение границы проходит через strtotime, поэтому работают как абсолютные даты, так и относительные выражения:
$request->validate([
'departure' => ['required', 'date', 'after:tomorrow'],
'return' => ['required', 'date', 'after:departure'],
]);
В первом случае дата вылета должна быть позже завтрашнего дня. Во втором - дата возврата после даты вылета. Под капотом Laravel сначала пробует распарсить аргумент как дату через strtotime/DateTime (см. ValidatesAttributes::compareDates). Если распарсить не удалось, аргумент трактуется как имя другого поля и берётся его значение. Поэтому ключевые слова today, tomorrow, yesterday всегда читаются как даты, даже если в запросе есть поле с таким именем - выбора между «полем» и «датой» нет, побеждает успешный парсинг.
after_or_equal и before_or_equal
Включительные варианты - дата может совпадать с границей:
$request->validate([
'check_in' => ['required', 'date', 'after_or_equal:today'],
'check_out' => ['required', 'date', 'after:check_in'],
]);
Заезд может быть сегодня (after_or_equal:today), а выезд - строго после заезда (after:check_in).
before и before_or_equal
Обратная сторона - верхняя граница:
$request->validate([
'birthday' => ['required', 'date', 'before:today'],
'start_date' => ['required', 'date', 'before_or_equal:end_date'],
]);
Дата рождения строго в прошлом. Начало периода - не позже конца.
Fluent-синтаксис для диапазонов
Rule::date() делает цепочки нагляднее, особенно с вычисляемыми границами:
use Illuminate\Validation\Rule;
$request->validate([
'event_date' => [
'required',
Rule::date()
->afterToday()
->before(now()->addMonths(6)),
],
'birth_date' => [
'required',
Rule::date()
->format('Y-m-d')
->beforeToday(),
],
]);
Доступные методы:
afterToday()- строго после сегодняtodayOrAfter()- сегодня или позжеbeforeToday()- строго до сегодняtodayOrBefore()- сегодня или раньшеafter($date)- после указанной датыafterOrEqual($date)- после или равнаbefore($date)- до указанной датыbeforeOrEqual($date)- до или равна
Относительные выражения strtotime
Строковая версия after и before принимает любое выражение, понятное strtotime:
$request->validate([
'delivery_date' => ['required', 'date', 'after:+3 days'],
'deadline' => ['required', 'date', 'before:+1 year'],
'meeting' => ['required', 'date', 'after:next monday'],
]);
Это удобно, но хрупко - строка +3 days вычисляется от текущего момента на сервере. В тестах это поведение нужно контролировать через travelTo().
Динамические границы
Иногда диапазон зависит от бизнес-логики - например, бронирование не дальше чем на 90 дней вперёд:
public function rules(): array
{
$maxDate = now()->addDays(90)->format('Y-m-d');
return [
'event_date' => [
'required',
'date_format:Y-m-d',
'after_or_equal:today',
"before_or_equal:{$maxDate}",
],
];
}
С fluent builder это же выглядит чище:
'event_date' => [
'required',
Rule::date()
->format('Y-m-d')
->todayOrAfter()
->beforeOrEqual(now()->addDays(90)),
],
Валидация времени
Отдельного правила time в Laravel нет. Время валидируется через date_format с соответствующим форматом:
$request->validate([
'start_time' => ['required', 'date_format:H:i'],
'precise_time' => ['required', 'date_format:H:i:s'],
]);
H:i принимает 14:30, H:i:s принимает 14:30:00. Если нужен 12-часовой формат с AM/PM:
'time' => ['required', 'date_format:g:i A'], // 2:30 PM
Время и дата вместе
Для полей datetime используйте составной формат:
$request->validate([
'starts_at' => ['required', 'date_format:Y-m-d H:i'],
'ends_at' => ['required', 'date_format:Y-m-d H:i', 'after:starts_at'],
]);
Сравнение after и before учитывает и дату, и время - значения парсятся целиком.
ISO 8601 с таймзоной
Для полей, которые приходят из JS-фронтенда в полном формате:
'starts_at' => ['required', 'date_format:Y-m-d\TH:i:sP'], // 2025-03-15T14:30:00+03:00
'ends_at' => ['required', 'date_format:Y-m-d\TH:i:sP', 'after:starts_at'],
Спецификатор P принимает смещение вида +03:00. Символ T между датой и временем экранируется обратным слешем.
Валидация timestamp
Unix timestamp - целое число секунд с 1 января 1970. Laravel не имеет отдельного правила для timestamp, но комбинация integer + min + max закрывает задачу:
$request->validate([
'created_after' => ['required', 'integer', 'min:0'],
'expires_at' => ['required', 'integer', 'min:0', 'max:4102444800'],
]);
max:4102444800 - это 2100 год. Верхняя граница защищает от абсурдных значений.
Для миллисекундных timestamp (JavaScript Date.now() отдаёт миллисекунды) делите на 1000 в prepareForValidation() или валидируйте как 13-значное число:
'js_timestamp' => ['required', 'numeric', 'digits:13'],
digits проверяет строковую длину, поэтому сочетается с numeric, а не с integer.
Если timestamp приходит как строка из формы, добавьте numeric вместо integer:
'timestamp' => ['required', 'numeric', 'min:0'],
numeric принимает и дробные числа, поэтому для строгой проверки целочисленного timestamp предпочтительнее integer.
Для API, где клиент передаёт timestamp, а сервер хранит datetime, конвертируйте в prepareForValidation():
protected function prepareForValidation(): void
{
if ($this->expires_at && is_numeric($this->expires_at)) {
$this->merge([
'expires_at' => Carbon::createFromTimestamp($this->expires_at)->format('Y-m-d H:i:s'),
]);
}
}
Или наоборот - для конвертации timestamp в Carbon после валидации:
$validated = $request->validated();
$date = Carbon::createFromTimestamp($validated['expires_at']);
Валидация года
Год - четырёхзначное число. Чистая дата-валидация здесь не нужна, хватит числовых правил:
$request->validate([
'birth_year' => ['required', 'integer', 'digits:4', 'min:1900', 'max:' . date('Y')],
'graduation' => ['required', 'integer', 'digits:4', 'min:2000', 'max:2100'],
]);
digits:4 гарантирует ровно 4 цифры. min / max задают допустимый диапазон. Для года рождения верхняя граница - текущий год, для будущих событий - разумный предел.
Если нужно проверить год + месяц (например, срок действия карты), используйте date_format:
'card_expiry' => ['required', 'date_format:m/Y', 'after:today'],
Формат m/Y принимает 03/2027. Правило after:today гарантирует, что карта ещё не просрочена.
Часовой пояс: timezone
Правило timezone проверяет значение по списку DateTimeZone::listIdentifiers():
$request->validate([
'tz' => ['required', 'timezone'],
]);
Принимает строки вроде Europe/Moscow, America/New_York, UTC. Можно фильтровать по региону:
// Только Европа
'tz' => ['required', 'timezone:Europe'],
// Только конкретная страна
'tz' => ['required', 'timezone:per_country,RU'],
// Все зоны (поведение по умолчанию)
'tz' => ['required', 'timezone:all'],
Фильтрация по стране полезна для форм, где пользователь уже указал страну - нет смысла показывать ему таймзоны Африки. Для России timezone:per_country,RU вернёт зоны от Калининграда до Камчатки.
Полный список регионов соответствует константам DateTimeZone: Africa, America, Antarctica, Arctic, Asia, Atlantic, Australia, Europe, Indian, Pacific. Значение all включает все зоны.
Проверка возраста
Прямого правила “минимальный возраст” нет, но before решает задачу:
$request->validate([
'birthday' => [
'required',
'date_format:Y-m-d',
'before:-18 years', // старше 18
'after:-120 years', // но не 200 лет
],
]);
before:-18 years означает: дата рождения должна быть раньше, чем 18 лет назад от сегодня. strtotime('-18 years') вычислит границу автоматически.
Для точного контроля через fluent builder:
'birthday' => [
'required',
Rule::date()
->format('Y-m-d')
->beforeOrEqual(now()->subYears(18))
->after(now()->subYears(120)),
],
Рабочие часы и слоты
Валидация времени в рамках рабочего дня требует комбинации date_format и кастомной проверки через after:
class AppointmentRequest extends FormRequest
{
public function rules(): array
{
return [
'date' => [
'required',
Rule::date()->format('Y-m-d')->todayOrAfter(),
],
'time' => ['required', 'date_format:H:i'],
];
}
public function after(): array
{
return [
function ($validator) {
$time = $this->input('time');
if ($time && ('09:00' > $time || '18:00' < $time)) {
$validator->errors()->add('time', 'Приём только с 09:00 до 18:00.');
}
},
];
}
}
Правилами after / before время само по себе не ограничить - они работают с полной датой. Для проверки только временного компонента нужен хук after() или кастомное правило.
Альтернативный подход - кастомный Rule-класс для переиспользования:
class WorkingHours implements ValidationRule
{
public function __construct(
private string $from = '09:00',
private string $to = '18:00',
) {}
// Строковое сравнение безопасно: date_format:H:i гарантирует двузначный формат
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if ($this->from > $value || $this->to < $value) {
$fail("Время :attribute должно быть с {$this->from} до {$this->to}.");
}
}
}
Использование:
'appointment_time' => ['required', 'date_format:H:i', new WorkingHours('10:00', '19:00')],
Подробнее о кастомных правилах - в пользовательские правила.
Периодические даты
Иногда нужно проверить, что дата приходится на определённый день недели - например, доставка только по будням:
public function after(): array
{
return [
function ($validator) {
$date = $this->validated('delivery_date') ?? null;
if ($date && Carbon::parse($date)->isWeekend()) {
$validator->errors()->add('delivery_date', 'Доставка по выходным не работает.');
}
},
];
}
Для проверки месяца, квартала или конкретного дня месяца работает та же схема - Carbon предоставляет методы isMonday(), isLastOfMonth(), quarter и другие.
Если доставка только в определённые даты (например, среда и пятница), логику можно завернуть в кастомное правило для переиспользования в нескольких формах. Подробнее - в пользовательские правила.
Хранение дат в базе данных
MySQL и PostgreSQL хранят даты в формате Y-m-d (DATE) и Y-m-d H:i:s (DATETIME/TIMESTAMP). Какой бы формат пользователь ни ввёл, к моменту вставки в базу дата должна быть в этом формате. Два подхода:
Первый - валидировать в нужном формате сразу (date_format:Y-m-d), чтобы validated-данные шли в модель напрямую.
Второй - принять удобный для пользователя формат (date_format:d.m.Y), конвертировать в prepareForValidation() или passedValidation(), и передать в модель. Второй подход гибче: форма может принимать 15.03.2025, а в базу ляжет 2025-03-15.
Если модель использует каст date или datetime, Eloquent сам конвертирует Carbon в нужный формат при записи. Главное, чтобы значение было валидной датой.
Полный пример: бронирование отеля
Форма бронирования с проверкой дат заезда, выезда и времени:
class BookingRequest extends FormRequest
{
public function rules(): array
{
return [
'check_in' => [
'required',
Rule::date()
->format('Y-m-d')
->todayOrAfter(),
],
'check_out' => [
'required',
Rule::date()
->format('Y-m-d')
->after('check_in'),
],
'arrival_time' => ['nullable', 'date_format:H:i'],
'guest_tz' => ['required', 'timezone'],
];
}
public function messages(): array
{
return [
'check_in.required' => 'Укажите дату заезда.',
'check_out.after' => 'Дата выезда должна быть позже даты заезда.',
];
}
}
Контроллер:
public function store(BookingRequest $request): RedirectResponse
{
$data = $request->validated();
$checkIn = Carbon::parse($data['check_in']);
$nights = $checkIn->diffInDays(Carbon::parse($data['check_out']));
$booking = Booking::create([
'check_in' => $data['check_in'],
'check_out' => $data['check_out'],
'nights' => $nights,
'timezone' => $data['guest_tz'],
]);
return to_route('bookings.show', $booking);
}
Полный пример: API-фильтр по датам
Фильтрация записей по диапазону дат - типичная задача для API. Оба поля опциональны, но если конечная дата указана, она не может быть раньше начальной:
class OrderFilterRequest extends FormRequest
{
public function rules(): array
{
return [
'date_from' => ['nullable', 'date_format:Y-m-d'],
'date_to' => ['nullable', 'date_format:Y-m-d', 'after_or_equal:date_from'],
'status' => ['nullable', 'in:pending,paid,shipped'],
];
}
}
public function index(OrderFilterRequest $request): JsonResponse
{
$query = Order::query();
if ($request->filled('date_from')) {
$query->whereDate('created_at', '>=', $request->validated('date_from'));
}
if ($request->filled('date_to')) {
$query->whereDate('created_at', '<=', $request->validated('date_to'));
}
return response()->json($query->paginate());
}
Правило after_or_equal:date_from не сработает, если date_from имеет значение null. Но если ключ date_from вообще отсутствует в запросе (не null, а не передан), поведение может отличаться. Для таких случаев добавьте sometimes к date_to - правила применятся только при наличии поля в запросе.
Для защиты от слишком широких диапазонов добавьте проверку в after():
public function after(): array
{
return [
function ($validator) {
$from = $this->validated('date_from');
$to = $this->validated('date_to');
if ($from && $to && Carbon::parse($from)->diffInDays(Carbon::parse($to)) > 365) {
$validator->errors()->add('date_to', 'Диапазон не может превышать 365 дней.');
}
},
];
}
Приведение типов через cast
После валидации дата приходит как строка. Для работы с ней в модели используйте каст:
protected function casts(): array
{
return [
'check_in' => 'date',
'check_out' => 'date',
'starts_at' => 'datetime',
'birth_year' => 'integer',
];
}
Каст date конвертирует в Carbon с обнулённым временем. datetime сохраняет и дату, и время. Можно указать формат хранения:
'event_date' => 'date:Y-m-d',
'starts_at' => 'datetime:Y-m-d H:i',
После каста доступны все методы Carbon:
$booking->check_in->format('d.m.Y');
$booking->check_in->diffInDays($booking->check_out);
$booking->starts_at->isPast();
При сериализации в JSON (API-ответы) Carbon по умолчанию выдаёт ISO 8601. Для другого формата переопределите serializeDate в модели:
protected function serializeDate(DateTimeInterface $date): string
{
return $date->format('Y-m-d H:i:s');
}
Мультизонные приложения
В приложениях с пользователями из разных часовых поясов возникает задача: хранить даты в UTC, показывать в локальном времени пользователя. Валидация в этом случае принимает дату в зоне пользователя и конвертирует в UTC:
class EventRequest extends FormRequest
{
public function rules(): array
{
return [
'starts_at' => ['required', 'date_format:Y-m-d H:i'],
'timezone' => ['required', 'timezone'],
];
}
protected function passedValidation(): void
{
$userTz = $this->validated('timezone');
$utc = Carbon::createFromFormat(
'Y-m-d H:i',
$this->validated('starts_at'),
$userTz
)->utc();
$this->merge(['starts_at' => $utc->format('Y-m-d H:i:s')]);
}
}
Конвертация происходит в passedValidation() - после проверки, но до попадания в контроллер. Контроллер получает дату уже в UTC и сохраняет её как есть. Обратная конвертация для отображения - на стороне фронтенда или в аксессоре модели.
Важно: config('app.timezone') по умолчанию UTC. Если вы поменяли её на Europe/Moscow, все даты в базе будут в московском времени. Для мультизонных приложений оставляйте UTC в конфиге и конвертируйте на уровне отображения.
prepareForValidation для дат
Пользователи вводят даты в разных форматах. Если форма принимает dd.mm.yyyy, но в базу нужен Y-m-d, приведите формат до валидации:
protected function prepareForValidation(): void
{
if ($this->check_in) {
$parsed = Carbon::createFromFormat('d.m.Y', $this->check_in);
if ($parsed) {
$this->merge(['check_in' => $parsed->format('Y-m-d')]);
}
}
}
public function rules(): array
{
return [
'check_in' => ['required', 'date_format:Y-m-d', 'after_or_equal:today'],
];
}
Другой подход - принять несколько форматов через date_format:Y-m-d,d.m.Y и привести к единому формату после валидации в passedValidation().
Даты в API: ISO 8601
Для API стандартом де-факто стал ISO 8601. Клиент отправляет 2025-03-15T14:30:00Z или 2025-03-15T14:30:00+03:00. Валидация:
$request->validate([
'starts_at' => ['required', 'date'],
'ends_at' => ['required', 'date', 'after:starts_at'],
]);
Здесь date подходит лучше, чем date_format, потому что ISO 8601 имеет варианты: с миллисекундами и без, с Z или со смещением +03:00. Правило date через strtotime принимает все эти варианты.
Если нужен строгий ISO без вариаций:
'starts_at' => ['required', 'date_format:Y-m-d\TH:i:sP'],
Формат P означает смещение вида +03:00. Буква T экранируется обратным слешем, чтобы не быть интерпретированной как спецификатор.
Условная валидация дат
Даты часто зависят от других полей. Время окончания обязательно, только если указано время начала:
$request->validate([
'start_date' => ['required', 'date_format:Y-m-d'],
'end_date' => ['required_with:start_date', 'nullable', 'date_format:Y-m-d', 'after:start_date'],
'start_time' => ['nullable', 'date_format:H:i'],
'end_time' => ['required_with:start_time', 'nullable', 'date_format:H:i'],
]);
required_with:start_date означает: end_date обязательна, если start_date заполнена. Подробнее об условных правилах - в условная валидация.
Ещё один паттерн - выбор между полной датой и относительным периодом. Пользователь указывает либо конкретные даты, либо период “последние 7 дней”:
$request->validate([
'period' => ['required', 'in:custom,7d,30d,90d'],
'date_from' => ['required_if:period,custom', 'nullable', 'date_format:Y-m-d'],
'date_to' => ['required_if:period,custom', 'nullable', 'date_format:Y-m-d', 'after_or_equal:date_from'],
]);
При period=custom обе даты обязательны. При period=30d они игнорируются, а контроллер вычислит диапазон сам.
Тестирование дат
Даты зависят от текущего времени, и тесты без фиксации даты будут нестабильными. Carbon позволяет заморозить время:
public function test_check_in_must_be_today_or_later(): void
{
$this->travelTo('2025-06-15');
$response = $this->post('/bookings', [
'check_in' => '2025-06-14',
'check_out' => '2025-06-16',
'guest_tz' => 'Europe/Moscow',
]);
$response->assertSessionHasErrors('check_in');
}
public function test_check_out_must_be_after_check_in(): void
{
$this->travelTo('2025-06-15');
$response = $this->post('/bookings', [
'check_in' => '2025-06-20',
'check_out' => '2025-06-18',
'guest_tz' => 'Europe/Moscow',
]);
$response->assertSessionHasErrors('check_out');
}
public function test_valid_booking_passes(): void
{
$this->travelTo('2025-06-15');
$response = $this->post('/bookings', [
'check_in' => '2025-06-20',
'check_out' => '2025-06-25',
'guest_tz' => 'Europe/Moscow',
]);
$response->assertSessionHasNoErrors();
}
travelTo() фиксирует “сейчас” для всего теста. Без этого after_or_equal:today будет давать разные результаты в разные дни.
Для API-тестов с датами:
public function test_api_date_filter(): void
{
Order::factory()->create(['created_at' => '2025-06-10']);
Order::factory()->create(['created_at' => '2025-06-20']);
$response = $this->getJson('/api/orders?date_from=2025-06-15');
$response->assertOk()
->assertJsonCount(1, 'data');
}
public function test_invalid_date_format_returns_422(): void
{
$response = $this->postJson('/api/events', [
'starts_at' => 'not-a-date',
]);
$response->assertStatus(422)
->assertJsonValidationErrors('starts_at');
}
Отдельно тестируйте граничные случаи: сегодняшняя дата при todayOrAfter, последний день месяца, 29 февраля в високосный и обычный год. Именно на границах чаще всего ломается валидация.
Для массового тестирования форматов удобен data provider:
#[DataProvider('invalidDates')]
public function test_rejects_invalid_dates(string $date): void
{
$response = $this->post('/events', ['starts_at' => $date]);
$response->assertSessionHasErrors('starts_at');
}
public static function invalidDates(): array
{
return [
'empty string' => [''],
'text' => ['not-a-date'],
'wrong format' => ['15/03/2025'],
'invalid day' => ['2025-02-30'],
];
}
Частые ошибки
date и date_format одновременно
// Ошибка: два парсера на одно поле
'birthday' => ['required', 'date', 'date_format:Y-m-d'],
date использует strtotime, date_format использует DateTime::createFromFormat. Два разных парсера на одно поле дают непредсказуемый результат. Выбирайте одно: date для гибкого ввода, date_format для строгого формата.
after:today vs afterToday()
Строковое after:today сравнивает через strtotime('today'), который возвращает полночь текущего дня. Если пользователь отправляет сегодняшнюю дату - она не пройдёт, потому что strtotime из неё тоже сделает полночь, а after требует строго больше. Для “сегодня или позже” используйте after_or_equal:today или Rule::date()->todayOrAfter().
Формат dd.mm.yyyy
// Не работает как ожидается
'date' => ['required', 'date_format:dd.mm.yyyy'],
// Правильно
'date' => ['required', 'date_format:d.m.Y'],
PHP использует одиночные буквы для форматов: d - день, m - месяц, Y - четырёхзначный год. Удвоенное dd интерпретируется как день + литерал d. Строчная y означает двузначный год (25), заглавная Y - четырёхзначный (2025).
Таймзона не влияет на сравнение
Правила after и before сравнивают даты без учёта таймзоны. Если пользователь в Europe/Moscow отправляет 2025-06-15 23:00, а сервер в UTC, сравнение пройдёт по серверному времени. Для корректной работы с таймзонами конвертируйте даты в prepareForValidation().
JavaScript-датапикер отправляет ISO 8601
Многие JS-компоненты (flatpickr, Vue datepicker) отправляют дату в формате 2025-03-15T14:30:00.000Z. Правило date это примет, а date_format:Y-m-d - нет. Если датапикер отправляет ISO 8601, либо настройте его формат вывода, либо обрежьте строку в prepareForValidation():
protected function prepareForValidation(): void
{
if ($this->event_date && str_contains($this->event_date, 'T')) {
$this->merge([
'event_date' => Carbon::parse($this->event_date)->format('Y-m-d'),
]);
}
}
Високосный год и 29 февраля
date_format:Y-m-d пропустит 2024-02-29 (високосный), но отвергнет 2025-02-29 (не високосный). Это корректное поведение - DateTime::createFromFormat проверяет существование даты. Но date с strtotime может повести себя иначе: strtotime('2025-02-29') тихо превратит дату в 2025-03-01. Ещё один повод предпочитать date_format для строгой валидации.
before без date / date_format
Правила after и before не проверяют формат сами по себе. Если написать 'start' => ['required', 'after:today'] без date или date_format, строка pizza пройдёт проверку required, а after получит невалидное значение и может повести себя непредсказуемо. Всегда комбинируйте с date или date_format:
// Правильно
'start' => ['required', 'date', 'after:today'],
// Тоже правильно
'start' => ['required', 'date_format:Y-m-d', 'after:today'],