WordPress uses cookies extensively for authentication (the wordpress_logged_in_* and wordpress_sec_* cookies), comment author remembering (comment_author_*), and the admin bar cookie. Plugins and themes often need to set their own cookies for: user preferences (dark mode toggle, dismissed banners, recently viewed products), multi-step wizard progress, A/B test buckets, or session-scoped data that does not require server-side storage. Unlike server-side data stored in wp_usermeta or transients, cookies persist across page views in the browser without a database query — making them suitable for lightweight client-state. The correct pattern in WordPress is to set cookies in the init hook (before any output) using PHP’s native setcookie(), and to read them from the $_COOKIE superglobal with proper sanitisation.
Problem: A site needs to: (1) remember whether a user dismissed a "Welcome" banner (cookie expires in 30 days), (2) store the user's preferred list/grid layout choice, and (3) set a GDPR-safe cookie only after the user explicitly clicks "Accept" on the cookie consent banner — all without JavaScript-only solutions, using PHP for reading and rendering server-side.
Solution: Use setcookie() in the init hook for all cookie writing (before headers are sent), read from $_COOKIE with sanitisation, and include the SameSite=Strict and Secure flags for security.
<?php
// ── Helper: set a cookie with secure defaults ─────────────────────────
function my_plugin_set_cookie( string $name, string $value, int $expiry_days = 30 ): void {
$secure = is_ssl();
$expires = time() + $expiry_days * DAY_IN_SECONDS;
// PHP 7.3+ array syntax for SameSite support:
setcookie( $name, $value, [
'expires' => $expires,
'path' => COOKIEPATH ?: '/',
'domain' => COOKIE_DOMAIN ?: '',
'secure' => $secure,
'httponly' => true, // JS cannot read it (protect auth-like cookies)
'samesite' => 'Strict', // CSRF protection
] );
}
// ── Helper: read + sanitise cookie value ──────────────────────────────
function my_plugin_get_cookie( string $name, string $default = '' ): string {
if ( ! isset( $_COOKIE[ $name ] ) ) {
return $default;
}
// Always sanitise cookie values — they are user-controlled
return sanitize_text_field( wp_unslash( $_COOKIE[ $name ] ) );
}
// ── Handle "dismiss banner" action ────────────────────────────────────
add_action( 'init', function () {
// User clicked "Dismiss" button (sent via GET ?dismiss_banner=1&nonce=...)
if ( '1' === ( $_GET['dismiss_banner'] ?? '' ) ) {
check_admin_referer( 'dismiss_banner', 'nonce' );
my_plugin_set_cookie( 'banner_dismissed', '1', 30 );
wp_safe_redirect( remove_query_arg( [ 'dismiss_banner', 'nonce' ] ) );
exit;
}
// Handle layout preference toggle
$layout = sanitize_key( $_GET['set_layout'] ?? '' );
if ( in_array( $layout, [ 'grid', 'list' ], true ) ) {
my_plugin_set_cookie( 'preferred_layout', $layout, 365 );
wp_safe_redirect( remove_query_arg( 'set_layout' ) );
exit;
}
// Handle GDPR cookie consent acceptance
if ( '1' === ( $_POST['accept_cookies'] ?? '' ) ) {
check_admin_referer( 'cookie_consent', 'consent_nonce' );
my_plugin_set_cookie( 'gdpr_consent', '1', 365 );
wp_safe_redirect( wp_get_referer() ?: home_url() );
exit;
}
} );
// ── Read cookies in templates ─────────────────────────────────────────
function should_show_banner(): bool {
return '1' !== my_plugin_get_cookie( 'banner_dismissed' );
}
function get_preferred_layout(): string {
$layout = my_plugin_get_cookie( 'preferred_layout', 'grid' );
return in_array( $layout, [ 'grid', 'list' ], true ) ? $layout : 'grid';
}
function has_gdpr_consent(): bool {
return '1' === my_plugin_get_cookie( 'gdpr_consent' );
}
// ── Render dismiss link with nonce ────────────────────────────────────
function render_banner(): void {
if ( ! should_show_banner() ) return;
$dismiss_url = wp_nonce_url(
add_query_arg( 'dismiss_banner', '1' ),
'dismiss_banner',
'nonce'
);
printf( '<div class="banner"><p>Welcome!</p><a href="%s">Dismiss</a></div>', esc_url( $dismiss_url ) );
}
NOTE: Cookies must be set before any output is sent to the browser (before the HTTP headers). In WordPress, the init hook runs before any template output and is the correct place to call setcookie(). Setting cookies inside template functions or in shortcodes is unreliable because output may have already started (especially with themes that use ob_start()). Always sanitise $_COOKIE values — cookies can be freely modified by the user and must never be trusted as safe data. For storing sensitive data (session tokens, user IDs), use WordPress's WP_Session_Tokens class or server-side storage (transients) and only store an opaque token in the cookie. The GDPR-safe pattern shown above only sets the tracking/preference cookie after explicit user consent — never set non-essential cookies automatically on page load.