Validating Date and Time Data
Trongate provides built-in validation rules for all HTML5 date and time input formats. These rules ensure data is in the correct format and represents valid dates and times.
Built-In Validation Rules
| Rule | Format Validated | Example | Use With |
|---|---|---|---|
valid_date |
YYYY-MM-DD |
2025-12-27 |
form_date() |
valid_time |
HH:MM or HH:MM:SS |
14:30 or 14:30:00 |
form_time() |
valid_datetime_local |
YYYY-MM-DDTHH:MM |
2025-12-27T14:30 |
form_datetime_local() |
valid_month |
YYYY-MM |
2025-12 |
form_month() |
valid_week |
YYYY-W## |
2025-W52 |
form_week() |
Basic Usage
Validating a Date
$this->validation->set_rules('due_date', 'due date', 'required|valid_date');Validating a Time
$this->validation->set_rules('start_time', 'start time', 'required|valid_time');Validating a DateTime
$this->validation->set_rules('appointment', 'appointment', 'required|valid_datetime_local');Validating a Month
$this->validation->set_rules('billing_month', 'billing month', 'required|valid_month');Validating a Week
$this->validation->set_rules('work_week', 'work week', 'required|valid_week');What Each Rule Validates
valid_date
Validates that the value:
- Matches the
YYYY-MM-DDformat exactly - Represents a valid calendar date
- Properly handles leap years (Feb 29 only in leap years)
- Rejects invalid dates like 2025-02-30 or 2025-13-01
// Valid examples
'2025-12-27' // ✓ Valid
'2024-02-29' // ✓ Valid (2024 is a leap year)
// Invalid examples
'2025-13-01' // ✗ Invalid (month 13 doesn't exist)
'2025-02-29' // ✗ Invalid (2025 is not a leap year)
'12/27/2025' // ✗ Invalid format
'2025-12-32' // ✗ Invalid (December only has 31 days)valid_time
Validates that the value:
- Matches
HH:MMorHH:MM:SSformat - Hours are between 00-23
- Minutes are between 00-59
- Seconds (if provided) are between 00-59
// Valid examples
'14:30' // ✓ Valid
'09:00' // ✓ Valid
'23:59' // ✓ Valid
'00:00:00' // ✓ Valid
'14:30:45' // ✓ Valid
// Invalid examples
'24:00' // ✗ Invalid (hour 24 doesn't exist)
'14:60' // ✗ Invalid (minute 60 doesn't exist)
'2:30 PM' // ✗ Invalid format
'14.30' // ✗ Invalid formatvalid_datetime_local
Validates that the value:
- Matches
YYYY-MM-DDTHH:MMformat exactly - Contains the literal "T" separator
- Date portion is valid (same rules as valid_date)
- Time portion is valid (same rules as valid_time)
// Valid examples
'2025-12-27T14:30' // ✓ Valid
'2024-02-29T00:00' // ✓ Valid (leap year)
// Invalid examples
'2025-12-27 14:30' // ✗ Invalid (space instead of T)
'2025-12-27T24:00' // ✗ Invalid (hour 24)
'2025-13-01T14:30' // ✗ Invalid (month 13)
'2025-12-27T14:30:00' // ✗ Invalid (includes seconds)valid_month
Validates that the value:
- Matches
YYYY-MMformat exactly - Year is a 4-digit number
- Month is between 01-12
// Valid examples
'2025-12' // ✓ Valid
'2025-01' // ✓ Valid
// Invalid examples
'2025-13' // ✗ Invalid (month 13)
'2025-00' // ✗ Invalid (month 00)
'25-12' // ✗ Invalid (2-digit year)
'2025/12' // ✗ Invalid formatvalid_week
Validates that the value:
- Matches
YYYY-W##format exactly - Contains the literal "W" prefix for week number
- Year is a 4-digit number
- Week number is between 01-53
// Valid examples
'2025-W52' // ✓ Valid
'2025-W01' // ✓ Valid
// Invalid examples
'2025-W00' // ✗ Invalid (week 00)
'2025-W54' // ✗ Invalid (week 54)
'2025-52' // ✗ Invalid (missing W)
'2025W52' // ✗ Invalid (missing hyphen)Combining with Other Rules
Date and time validation rules work seamlessly with other validation rules:
// Required date
$this->validation->set_rules('due_date', 'due date', 'required|valid_date');
// Optional date (validates format only if provided)
$this->validation->set_rules('completion_date', 'completion date', 'valid_date');
// Required time with custom callback
$this->validation->set_rules('meeting_time', 'meeting time', 'required|valid_time|callback_during_business_hours');
// Optional month
$this->validation->set_rules('report_month', 'report month', 'valid_month');Optional Fields: If a date/time field is optional (not required), the validation rule will only check the format when a value is provided. An empty value will pass validation.
Complete Form Example
public function submit(): void {
// Set validation rules
$this->validation->set_rules('event_title', 'event title', 'required|min_length[3]');
$this->validation->set_rules('event_date', 'event date', 'required|valid_date');
$this->validation->set_rules('start_time', 'start time', 'required|valid_time');
$this->validation->set_rules('end_time', 'end time', 'required|valid_time|callback_end_after_start');
if ($this->validation->run() === true) {
$data['event_title'] = post('event_title', true);
$data['event_date'] = post('event_date', true);
$data['start_time'] = post('start_time', true);
$data['end_time'] = post('end_time', true);
$this->db->insert($data, 'events');
set_flashdata('Event created successfully');
redirect('events/manage');
} else {
$this->create();
}
}Custom Validation with Callbacks
For business logic validation beyond format checking, use custom callback methods:
Date Range Validation
public function submit(): void {
$this->validation->set_rules('start_date', 'start date', 'required|valid_date');
$this->validation->set_rules('end_date', 'end date', 'required|valid_date|callback_end_after_start');
if ($this->validation->run() === true) {
// Process form
} else {
$this->create();
}
}
public function callback_end_after_start($end_date): string|bool {
$start_date = post('start_date', true);
if ($start_date === '' || $end_date === '') {
return true; // Let required rule handle empty values
}
if (strtotime($end_date) <= strtotime($start_date)) {
return 'The {label} must be after the start date.';
}
return true;
}Time Range Validation
public function submit(): void {
$this->validation->set_rules('opening_time', 'opening time', 'required|valid_time');
$this->validation->set_rules('closing_time', 'closing time', 'required|valid_time|callback_closing_after_opening');
if ($this->validation->run() === true) {
// Process form
} else {
$this->create();
}
}
public function callback_closing_after_opening($closing_time): string|bool {
$opening_time = post('opening_time', true);
if ($opening_time === '' || $closing_time === '') {
return true;
}
if (strtotime($closing_time) <= strtotime($opening_time)) {
return 'The {label} must be after the opening time.';
}
return true;
}Future Date Only
public function submit(): void {
$this->validation->set_rules('event_date', 'event date', 'required|valid_date|callback_must_be_future');
if ($this->validation->run() === true) {
// Process form
} else {
$this->create();
}
}
public function callback_must_be_future($date): string|bool {
if ($date === '') {
return true;
}
$today = date('Y-m-d');
if (strtotime($date) < strtotime($today)) {
return 'The {label} must be today or in the future.';
}
return true;
}Past Date Only
public function callback_must_be_past($date): string|bool {
if ($date === '') {
return true;
}
$today = date('Y-m-d');
if (strtotime($date) > strtotime($today)) {
return 'The {label} cannot be in the future.';
}
return true;
}Business Hours Validation
public function callback_during_business_hours($time): string|bool {
if ($time === '') {
return true;
}
$hour = (int) substr($time, 0, 2);
if ($hour < 9 || $hour >= 17) {
return 'The {label} must be between 9:00 AM and 5:00 PM.';
}
return true;
}Weekday Only Validation
public function callback_must_be_weekday($date): string|bool {
if ($date === '') {
return true;
}
$day_of_week = (int) date('N', strtotime($date)); // 1=Monday, 7=Sunday
if ($day_of_week >= 6) {
return 'The {label} must be a weekday (Monday-Friday).';
}
return true;
}Date Within Range
public function callback_within_90_days($date): string|bool {
if ($date === '') {
return true;
}
$today = strtotime(date('Y-m-d'));
$selected = strtotime($date);
$max_date = strtotime('+90 days', $today);
if ($selected < $today || $selected > $max_date) {
return 'The {label} must be within the next 90 days.';
}
return true;
}Minimum Age Validation
public function callback_minimum_age_18($birth_date): string|bool {
if ($birth_date === '') {
return true;
}
$today = new DateTime();
$dob = new DateTime($birth_date);
$age = $today->diff($dob)->y;
if ($age < 18) {
return 'You must be at least 18 years old.';
}
return true;
}DateTime Range Validation
public function callback_event_end_after_start($event_end): string|bool {
$event_start = post('event_start', true);
if ($event_start === '' || $event_end === '') {
return true;
}
// Convert to timestamps for comparison
$start = strtotime(str_replace('T', ' ', $event_start));
$end = strtotime(str_replace('T', ' ', $event_end));
if ($end <= $start) {
return 'The {label} must be after the event start time.';
}
// Check if event is too long (e.g., max 8 hours)
$duration_hours = ($end - $start) / 3600;
if ($duration_hours > 8) {
return 'Event duration cannot exceed 8 hours.';
}
return true;
}Appointment Slot Validation
public function callback_valid_appointment_time($datetime): string|bool {
if ($datetime === '') {
return true;
}
// Convert to DateTime object
$dt = new DateTime(str_replace('T', ' ', $datetime));
// Check if weekend
$day_of_week = (int) $dt->format('N');
if ($day_of_week >= 6) {
return 'Appointments are only available Monday through Friday.';
}
// Check if during business hours
$hour = (int) $dt->format('H');
if ($hour < 9 || $hour >= 17) {
return 'Appointments must be between 9:00 AM and 5:00 PM.';
}
// Check if on the hour or half-hour
$minute = (int) $dt->format('i');
if ($minute !== 0 && $minute !== 30) {
return 'Appointments must be scheduled on the hour or half-hour.';
}
// Check if slot is available
$sql = "SELECT COUNT(*) as count FROM appointments
WHERE scheduled_at = :datetime";
$result = $this->db->query_bind($sql, ['datetime' => $dt->format('Y-m-d H:i:s')], 'object');
if ($result->count > 0) {
return 'This appointment slot is already booked.';
}
return true;
}Validation Error Messages
Default error messages for built-in rules:
| Rule | Default Error Message |
|---|---|
valid_date |
"The [field label] must be a valid date in YYYY-MM-DD format." |
valid_time |
"The [field label] must be a valid time in HH:MM or HH:MM:SS format." |
valid_datetime_local |
"The [field label] must be a valid datetime in YYYY-MM-DDTHH:MM format." |
valid_month |
"The [field label] must be a valid month in YYYY-MM format." |
valid_week |
"The [field label] must be a valid week in YYYY-W## format." |
Real-World Validation Scenarios
Booking System
public function submit_booking(): void {
$this->validation->set_rules('booking_date', 'booking date', 'required|valid_date|callback_valid_booking_date');
$this->validation->set_rules('time_slot', 'time slot', 'required|valid_time|callback_available_time_slot');
if ($this->validation->run() === true) {
// Process booking
} else {
$this->booking_form();
}
}
public function callback_valid_booking_date($date): string|bool {
// Must be at least 24 hours in advance
$tomorrow = date('Y-m-d', strtotime('+1 day'));
if (strtotime($date) < strtotime($tomorrow)) {
return 'Bookings must be made at least 24 hours in advance.';
}
// Cannot be more than 90 days in advance
$max_date = date('Y-m-d', strtotime('+90 days'));
if (strtotime($date) > strtotime($max_date)) {
return 'Bookings cannot be made more than 90 days in advance.';
}
return true;
}
public function callback_available_time_slot($time): string|bool {
$date = post('booking_date', true);
if ($date === '' || $time === '') {
return true;
}
// Check if slot is available
$sql = "SELECT COUNT(*) as count FROM bookings
WHERE booking_date = :date AND time_slot = :time";
$result = $this->db->query_bind($sql, [
'date' => $date,
'time' => $time
], 'object');
if ($result->count > 0) {
return 'This time slot is no longer available.';
}
return true;
}Project Timeline
public function submit(): void {
$this->validation->set_rules('project_start', 'project start date', 'required|valid_date|callback_reasonable_start_date');
$this->validation->set_rules('project_end', 'project end date', 'required|valid_date|callback_reasonable_timeline');
if ($this->validation->run() === true) {
// Process project
} else {
$this->create();
}
}
public function callback_reasonable_start_date($start_date): string|bool {
$today = date('Y-m-d');
$max_future = date('Y-m-d', strtotime('+1 year'));
if (strtotime($start_date) < strtotime($today)) {
return 'The {label} cannot be in the past.';
}
if (strtotime($start_date) > strtotime($max_future)) {
return 'The {label} cannot be more than 1 year in the future.';
}
return true;
}
public function callback_reasonable_timeline($end_date): string|bool {
$start_date = post('project_start', true);
if ($start_date === '' || $end_date === '') {
return true;
}
$start = strtotime($start_date);
$end = strtotime($end_date);
// End must be after start
if ($end <= $start) {
return 'The {label} must be after the project start date.';
}
// Project must be at least 1 week long
$min_end = strtotime('+1 week', $start);
if ($end < $min_end) {
return 'Project must be at least 1 week long.';
}
// Project cannot exceed 2 years
$max_end = strtotime('+2 years', $start);
if ($end > $max_end) {
return 'Project duration cannot exceed 2 years.';
}
return true;
}Combining Multiple Validations
public function submit_event(): void {
// Event details
$this->validation->set_rules('event_title', 'event title', 'required|min_length[5]');
// Date and time validations
$this->validation->set_rules('event_date', 'event date', 'required|valid_date|callback_must_be_future|callback_must_be_weekday');
$this->validation->set_rules('start_time', 'start time', 'required|valid_time|callback_during_business_hours');
$this->validation->set_rules('end_time', 'end time', 'required|valid_time|callback_end_after_start|callback_reasonable_duration');
// Registration deadline
$this->validation->set_rules('registration_deadline', 'registration deadline', 'required|valid_datetime_local|callback_before_event');
if ($this->validation->run() === true) {
// Process event
} else {
$this->create();
}
}Important Notes
- Built-in validation rules check format AND validity (e.g., valid_date rejects Feb 30)
- Use callbacks for business logic validation (ranges, availability, etc.)
- Empty values pass validation unless
requiredis specified - Always return
trueor an error string from callbacks - Use
{label}in error messages for automatic label substitution - Callbacks should handle empty values gracefully (let
requiredrule handle them) - Browser validation is a convenience, not security - always validate server-side
- Consider timezone implications when validating datetime values
Best Practice: Use built-in rules for format validation and custom callbacks for business logic. This separation keeps your code clean and maintainable.
We're continually improving the Trongate documentation. If anything is incorrect, unclear, incomplete, or could be better, we'd genuinely appreciate your input.
Share your thoughts in the Documentation Feedback.