Booking bar form with date validation
A date and guests booking bar shortcode with a nonce, server-side validation and a redirect to a results page.
WordPressFormsbookingformadmin-postvalidation
Live preview
Code
<?php
/**
* Booking bar: shortcode + admin-post handler.
*
* @package Acme_Theme
*/
defined( 'ABSPATH' ) || exit;
function acme_booking_bar(): string {
ob_start();
?>
<form class="acme-booking" method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<input type="hidden" name="action" value="acme_booking_search">
<?php wp_nonce_field( 'acme_booking_search', 'acme_nonce' ); ?>
<label>
<?php esc_html_e( 'Check in', 'acme-theme' ); ?>
<input type="date" name="check_in" min="<?php echo esc_attr( wp_date( 'Y-m-d' ) ); ?>" required>
</label>
<label>
<?php esc_html_e( 'Check out', 'acme-theme' ); ?>
<input type="date" name="check_out" required>
</label>
<label>
<?php esc_html_e( 'Guests', 'acme-theme' ); ?>
<input type="number" name="guests" value="2" min="1" max="8" required>
</label>
<button type="submit"><?php esc_html_e( 'Check availability', 'acme-theme' ); ?></button>
</form>
<?php
return (string) ob_get_clean();
}
add_shortcode( 'acme_booking_bar', 'acme_booking_bar' );
function acme_booking_handle(): void {
$back = wp_get_referer() ?: home_url( '/' );
if ( ! isset( $_POST['acme_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['acme_nonce'] ) ), 'acme_booking_search' ) ) {
wp_safe_redirect( add_query_arg( 'booking_error', 'nonce', $back ) );
exit;
}
$in = sanitize_text_field( wp_unslash( $_POST['check_in'] ?? '' ) );
$out = sanitize_text_field( wp_unslash( $_POST['check_out'] ?? '' ) );
$guests = min( 8, max( 1, absint( $_POST['guests'] ?? 1 ) ) );
$d_in = DateTimeImmutable::createFromFormat( '!Y-m-d', $in );
$d_out = DateTimeImmutable::createFromFormat( '!Y-m-d', $out );
$today = new DateTimeImmutable( 'today', wp_timezone() );
if ( ! $d_in || ! $d_out || $d_in < $today || $d_out <= $d_in ) {
wp_safe_redirect( add_query_arg( 'booking_error', 'dates', $back ) );
exit;
}
wp_safe_redirect( add_query_arg( array( 'in' => $in, 'out' => $out, 'guests' => $guests ), home_url( '/rooms/' ) ) );
exit;
}
add_action( 'admin_post_nopriv_acme_booking_search', 'acme_booking_handle' );
add_action( 'admin_post_acme_booking_search', 'acme_booking_handle' );
document.querySelectorAll('.acme-booking').forEach((form) => {
const [inEl, outEl] = form.querySelectorAll('input[type="date"]');
inEl.addEventListener('change', () => {
// Check out must be at least one night after check in.
const next = new Date(inEl.value);
next.setDate(next.getDate() + 1);
outEl.min = next.toISOString().slice(0, 10);
if (outEl.value && outEl.value < outEl.min) outEl.value = outEl.min;
});
});
How it works
What it solves
A hospitality site needs a compact date and guest search at the top of the page. The browser can hint at valid dates, but only the server can be trusted, so this bar pairs friendly inputs with strict validation and a safe redirect.
How it works
The shortcode prints a form that posts to admin-post.php with an action name and a nonce. The handler is hooked for both logged-out and logged-in visitors. It checks the nonce, sanitises every field, parses dates with an exact Y-m-d format in the site's timezone, rejects past or reversed ranges, clamps the guest count, and redirects with query arguments.
A few lines of JavaScript keep the check-out min one night after the check-in value.
Adapting it
Point the redirect at your results page or a booking plugin's URL and read in, out and guests from $_GET there, sanitising again. Show booking_error messages near the form. Add a children field or room type select by following the same sanitise-then-validate steps.
Pitfalls
- Nonces expire, and cached pages can serve an old one; if the bar is on a cached page, load the nonce by AJAX or accept the redirect error and retry.
- Always use
wp_safe_redirectfor internal targets. createFromFormatwith!resets unparsed fields to zero, so comparisons are exact.- The browser
minattribute is a convenience, not security.
How to use it
Save as inc/booking-bar.php, require it from functions.php, and enqueue assets/booking-bar.js.
Place [acme_booking_bar] in a hero or header pattern.
Build the page that receives in, out and guests to list available items.
Licence: GPL-2.0-or-later. This snippet comes from WordPress code and is GPL-2.0-or-later.