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-DD format 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:MM or HH:MM:SS format
- 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 format
valid_datetime_local
Validates that the value:
- Matches
YYYY-MM-DDTHH:MM format 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-MM format 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 format
valid_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
required is specified
- Always return
true or an error string from callbacks
- Use
{label} in error messages for automatic label substitution
- Callbacks should handle empty values gracefully (let
required rule 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.