WordPress Development Recipe: Real-time custom event triggers using WebSockets and Filesystem API
Leveraging WebSockets and the Filesystem API for Real-time WordPress Events
This recipe details a robust method for implementing real-time event triggers within WordPress, bypassing traditional polling mechanisms. We’ll architect a solution that utilizes WebSockets for instant bidirectional communication and the WordPress Filesystem API for persistent, event-driven state management. This approach is particularly useful for scenarios requiring immediate updates to the frontend based on backend actions, such as live comment notifications, real-time form submission feedback, or dynamic content updates without page reloads.
Prerequisites and Setup
Before diving into the code, ensure you have the following:
- A WordPress installation (local or remote).
- Composer installed for managing PHP dependencies.
- A basic understanding of PHP, JavaScript, and WordPress plugin development.
- A WebSocket server implementation. For this example, we’ll assume a simple Node.js server using the
wslibrary. You’ll need Node.js and npm/yarn installed.
WebSocket Server Implementation (Node.js)
This Node.js script acts as our central hub for WebSocket communication. It listens for incoming connections, broadcasts messages to all connected clients, and can be extended to handle specific routing or authentication.
First, set up a new Node.js project:
mkdir wordpress-websocket-server cd wordpress-websocket-server npm init -y npm install ws
Create a file named server.js and add the following code:
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
console.log('WebSocket server started on port 8080');
wss.on('connection', (ws) => {
console.log('Client connected');
ws.on('message', (message) => {
console.log(`Received message => ${message}`);
// In a real-world scenario, you might parse this message
// and trigger specific actions in WordPress.
// For now, we'll just broadcast it back.
wss.clients.forEach((client) => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(`Broadcast: ${message}`);
}
});
});
ws.on('close', () => {
console.log('Client disconnected');
});
ws.on('error', (error) => {
console.error('WebSocket error:', error);
});
// Send a welcome message to the newly connected client
ws.send('Welcome to the WordPress WebSocket server!');
});
// Function to broadcast events from WordPress
function broadcastEvent(eventData) {
const message = JSON.stringify(eventData);
console.log(`Broadcasting: ${message}`);
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
// Export the broadcast function for use in other modules
module.exports = { broadcastEvent };
To run the server, execute:
node server.js
WordPress Plugin Structure
We’ll create a simple WordPress plugin to handle the integration. Create a new directory wp-realtime-events in your wp-content/plugins/ directory. Inside it, create the main plugin file, wp-realtime-events.php.
<?php
/**
* Plugin Name: WP Realtime Events
* Description: Integrates WebSockets for real-time event triggers using the Filesystem API.
* Version: 1.0
* Author: Your Name
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
// Define constants for filesystem paths
define( 'WPRE_EVENT_DIR', trailingslashit( WP_CONTENT_DIR . '/realtime-events' ) );
define( 'WPRE_EVENT_FILE', WPRE_EVENT_DIR . 'latest_event.json' );
// Include necessary files
require_once plugin_dir_path( __FILE__ ) . 'includes/class-wpre-websocket-client.php';
require_once plugin_dir_path( __FILE__ ) . 'includes/class-wpre-event-manager.php';
// Initialize classes
$wpre_websocket_client = new WPRE_WebSocket_Client();
$wpre_event_manager = new WPRE_Event_Manager();
// Hook into WordPress actions and filters
register_activation_hook( __FILE__, array( $wpre_event_manager, 'activate' ) );
add_action( 'admin_init', array( $wpre_websocket_client, 'init' ) );
add_action( 'wp_loaded', array( $wpre_event_manager, 'setup_event_listeners' ) );
// Example of triggering an event (e.g., on post save)
add_action( 'save_post', array( $wpre_event_manager, 'trigger_post_save_event' ), 10, 3 );
?>
Filesystem API for Event State
We’ll use WordPress’s built-in Filesystem API to store the latest event data. This provides a reliable way to persist event information that can be read by the WebSocket client and potentially by other WordPress processes.
Create a directory structure: wp-realtime-events/includes/. Inside includes/, create class-wpre-event-manager.php.
<?php
/**
* Manages real-time event triggers and state using the Filesystem API.
*/
class WPRE_Event_Manager {
/**
* Runs on plugin activation to create the event directory.
*/
public function activate() {
if ( ! file_exists( WPRE_EVENT_DIR ) ) {
wp_mkdir_p( WPRE_EVENT_DIR );
}
// Initialize with an empty event
$this->save_event_data( array( 'timestamp' => time(), 'type' => 'initialized' ) );
}
/**
* Sets up listeners for various WordPress actions that should trigger events.
*/
public function setup_event_listeners() {
// Add more hooks here as needed for different event types.
// For example: 'comment_post', 'user_register', etc.
}
/**
* Triggers an event when a post is saved.
*
* @param int $post_id Post ID.
* @param WP_Post $post Post object.
* @param bool $update Whether this is an existing post being updated.
*/
public function trigger_post_save_event( $post_id, $post, $update ) {
// Avoid triggering on autosaves and revisions
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( wp_is_post_revision( $post_id ) ) {
return;
}
// Ensure we are working with a valid post type if necessary
// if ( 'post' !== $post->post_type ) {
// return;
// }
$event_data = array(
'timestamp' => time(),
'type' => 'post_saved',
'post_id' => $post_id,
'post_title'=> $post->post_title,
'status' => $post->post_status,
'is_update' => $update,
);
$this->trigger_event( $event_data );
}
/**
* Triggers a real-time event.
*
* @param array $event_data The data associated with the event.
*/
public function trigger_event( $event_data ) {
$this->save_event_data( $event_data );
// The WebSocket client will pick this up and broadcast it.
}
/**
* Saves event data to a JSON file using the WordPress Filesystem API.
*
* @param array $data The data to save.
* @return bool True on success, false on failure.
*/
protected function save_event_data( $data ) {
global $wp_filesystem;
// Ensure the filesystem is ready
if ( ! $wp_filesystem ) {
// Attempt to initialize the filesystem if not already done
if ( ! WP_Filesystem() ) {
error_log( 'WPRE_Event_Manager: Failed to initialize WordPress filesystem.' );
return false;
}
}
// Ensure the directory exists
if ( ! file_exists( WPRE_EVENT_DIR ) ) {
if ( ! $wp_filesystem->mkdir( WPRE_EVENT_DIR ) ) {
error_log( 'WPRE_Event_Manager: Failed to create directory: ' . WPRE_EVENT_DIR );
return false;
}
}
$json_data = wp_json_encode( $data, JSON_PRETTY_PRINT );
if ( false === $json_data ) {
error_log( 'WPRE_Event_Manager: Failed to encode event data to JSON.' );
return false;
}
// Use put_contents for simplicity, but consider WP_Filesystem::put_contents for more robust handling
if ( ! $wp_filesystem->put_contents( WPRE_EVENT_FILE, $json_data, 0644 ) ) {
error_log( 'WPRE_Event_Manager: Failed to write event data to file: ' . WPRE_EVENT_FILE );
return false;
}
return true;
}
/**
* Retrieves the latest event data from the file.
*
* @return array|false Event data array on success, false on failure or if file doesn't exist.
*/
public function get_latest_event_data() {
global $wp_filesystem;
if ( ! $wp_filesystem ) {
if ( ! WP_Filesystem() ) {
error_log( 'WPRE_Event_Manager: Failed to initialize WordPress filesystem for reading.' );
return false;
}
}
if ( ! file_exists( WPRE_EVENT_FILE ) ) {
return false; // No events yet
}
$file_content = $wp_filesystem->get_contents( WPRE_EVENT_FILE );
if ( false === $file_content ) {
error_log( 'WPRE_Event_Manager: Failed to read event data from file: ' . WPRE_EVENT_FILE );
return false;
}
$data = json_decode( $file_content, true );
if ( json_last_error() !== JSON_ERROR_NONE ) {
error_log( 'WPRE_Event_Manager: Failed to decode JSON from event file. Error: ' . json_last_error_msg() );
return false;
}
return $data;
}
}
WebSocket Client Integration
This class will handle the connection to our Node.js WebSocket server. It will periodically check the filesystem for new event data and send it over the WebSocket connection. For simplicity, we’ll use a cron job or a scheduled event to trigger this check. A more advanced approach might involve a persistent background process or a server-side PHP WebSocket client library.
Create wp-realtime-events/includes/class-wpre-websocket-client.php.
<?php
/**
* Handles the WebSocket client connection and event broadcasting from WordPress.
*/
class WPRE_WebSocket_Client {
private $ws_server_url;
private $event_manager;
public function __construct() {
// Configure your WebSocket server URL here.
// For local development, it might be 'ws://localhost:8080'
// For production, ensure this is accessible and potentially secured.
$this->ws_server_url = defined('WPRE_WS_SERVER_URL') ? WPRE_WS_SERVER_URL : 'ws://localhost:8080';
$this->event_manager = new WPRE_Event_Manager(); // Instantiate here for direct access
}
/**
* Initializes the WebSocket client, typically hooked into admin_init.
*/
public function init() {
// We'll use a scheduled event (WP Cron) to periodically check for events.
// This is a simplified approach. A more robust solution might use
// a persistent PHP WebSocket client or a dedicated microservice.
if ( ! wp_next_scheduled( 'wpre_websocket_heartbeat' ) ) {
wp_schedule_event( time(), 'hourly', 'wpre_websocket_heartbeat' ); // Adjust frequency as needed
}
add_action( 'wpre_websocket_heartbeat', array( $this, 'send_pending_events' ) );
// Optional: Add an AJAX endpoint to manually trigger a check or send events.
// add_action( 'wp_ajax_wpre_send_event', array( $this, 'ajax_send_event' ) );
}
/**
* Checks for new events in the filesystem and sends them via WebSocket.
*/
public function send_pending_events() {
// This method is intended to be called by WP Cron.
// It needs to establish a WebSocket connection and send data.
// Note: WP Cron runs via HTTP requests, so this is not a true
// persistent connection. For real-time, a server-side PHP WebSocket
// client or a Node.js/other language service is more appropriate.
// For demonstration, we'll simulate sending. A real implementation
// would require a PHP WebSocket client library (e.g., Ratchet, Swoole).
$latest_event = $this->event_manager->get_latest_event_data();
if ( $latest_event && isset( $latest_event['timestamp'] ) ) {
// In a real scenario, you'd compare this timestamp with the last sent event
// to avoid resending. For simplicity, we'll assume WP Cron handles frequency.
// --- SIMULATION of sending via WebSocket ---
// This part requires a PHP WebSocket client library.
// Example using a hypothetical library:
/*
try {
$client = new WebSocketClient($this->ws_server_url);
$client->connect();
$client->send(json_encode($latest_event));
$client->disconnect();
error_log('WPRE_WebSocket_Client: Sent event: ' . json_encode($latest_event));
} catch (Exception $e) {
error_log('WPRE_WebSocket_Client: Failed to send event via WebSocket: ' . $e->getMessage());
}
*/
// For this example, we'll just log that an event would be sent.
error_log('WPRE_WebSocket_Client: Would send event via WebSocket: ' . json_encode($latest_event));
// In a production setup, you'd want to ensure the event is only sent once.
// This might involve storing the timestamp of the last sent event.
}
}
/**
* AJAX handler to manually trigger sending an event.
* Useful for testing.
*/
public function ajax_send_event() {
check_ajax_referer( 'wpre_send_event_nonce', 'nonce' );
$event_data = isset( $_POST['event_data'] ) ? json_decode( stripslashes( $_POST['event_data'] ), true ) : array();
if ( empty( $event_data ) ) {
wp_send_json_error( 'Invalid event data provided.' );
}
// Trigger the event, which saves it to the file.
// The send_pending_events (via cron) would normally pick this up.
// For AJAX, we can attempt to send immediately if a direct PHP WebSocket client is available.
if ( $this->event_manager->trigger_event( $event_data ) ) {
// If you have a direct PHP WebSocket client library integrated here,
// you could call its send method directly.
// For now, we'll just confirm the event was saved.
wp_send_json_success( 'Event triggered and saved. Waiting for WebSocket heartbeat to broadcast.' );
} else {
wp_send_json_error( 'Failed to trigger event.' );
}
}
/**
* Cleans up the scheduled event on plugin deactivation.
*/
public static function deactivate() {
$timestamp = wp_next_scheduled( 'wpre_websocket_heartbeat' );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, 'wpre_websocket_heartbeat' );
}
}
}
Frontend JavaScript Integration
On the frontend, we need JavaScript to establish a WebSocket connection and listen for incoming messages. This script should be enqueued on the pages where you want real-time updates.
Add the following JavaScript to your theme’s JavaScript files or enqueue it as a separate script within your plugin.
// In your plugin's main file (wp-realtime-events.php) or theme's functions.php:
function wpre_enqueue_realtime_scripts() {
// Only enqueue on frontend pages where needed
if ( ! is_admin() ) {
wp_enqueue_script( 'wpre-websocket-client', plugin_dir_url( __FILE__ ) . 'js/websocket-client.js', array(), '1.0', true );
// Pass WebSocket server URL to the script
wp_localize_script( 'wpre-websocket-client', 'wpre_ajax_object', array(
'ws_server_url' => defined('WPRE_WS_SERVER_URL') ? WPRE_WS_SERVER_URL : 'ws://localhost:8080',
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'wpre_send_event_nonce' )
));
}
}
add_action( 'wp_enqueue_scripts', 'wpre_enqueue_realtime_scripts' );
Create the JavaScript file: wp-realtime-events/js/websocket-client.js.
document.addEventListener('DOMContentLoaded', function() {
if (typeof wpre_ajax_object === 'undefined') {
console.error('wpre_ajax_object not defined. Ensure scripts are enqueued correctly.');
return;
}
const wsUrl = wpre_ajax_object.ws_server_url;
let websocket = null;
function connectWebSocket() {
console.log(`Attempting to connect to WebSocket server at: ${wsUrl}`);
websocket = new WebSocket(wsUrl);
websocket.onopen = function(event) {
console.log('WebSocket connection opened:', event);
// Send initial message or data if needed
websocket.send('Hello from WordPress frontend!');
};
websocket.onmessage = function(event) {
console.log('WebSocket message received:', event.data);
try {
const data = JSON.parse(event.data);
// Handle the received event data here
handleRealtimeEvent(data);
} catch (e) {
console.error('Failed to parse WebSocket message:', e);
}
};
websocket.onclose = function(event) {
console.log('WebSocket connection closed:', event);
// Attempt to reconnect after a delay
setTimeout(connectWebSocket, 5000); // Reconnect every 5 seconds
};
websocket.onerror = function(event) {
console.error('WebSocket error:', event);
// The 'onclose' event will also be fired after an error
};
}
function handleRealtimeEvent(eventData) {
// Example: Display a notification for a post save event
if (eventData.type === 'post_saved') {
const message = `Post "${eventData.post_title}" (ID: ${eventData.post_id}) was saved.`;
console.log(message);
// You could update UI elements, show notifications, etc.
// Example: Append to a log element
const logElement = document.getElementById('realtime-event-log');
if (logElement) {
const p = document.createElement('p');
p.textContent = `[${new Date(eventData.timestamp * 1000).toLocaleTimeString()}] ${message}`;
logElement.prepend(p); // Add to the top
}
} else if (eventData.type === 'initialized') {
console.log('WebSocket server initialized.');
}
// Add more event type handlers as needed
}
// Start the WebSocket connection
connectWebSocket();
// --- Optional: AJAX to trigger event from frontend (for testing) ---
// Example: A button to trigger a dummy event
const triggerButton = document.getElementById('trigger-dummy-event');
if (triggerButton) {
triggerButton.addEventListener('click', function() {
const dummyEvent = {
type: 'frontend_triggered_test',
message: 'This is a test event from the frontend.',
timestamp: Date.now() / 1000
};
// Send via AJAX to WordPress backend, which then saves to file
// and the Node.js server would ideally pick it up.
// Note: This AJAX call doesn't directly send to WebSocket.
// It relies on the WordPress backend to process and potentially broadcast.
fetch(wpre_ajax_object.ajax_url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
action: 'wpre_send_event',
nonce: wpre_ajax_object.nonce,
event_data: JSON.stringify(dummyEvent)
})
})
.then(response => response.json())
.then(data => {
console.log('AJAX response for triggering event:', data);
if (data.success) {
alert('Dummy event sent to WordPress backend!');
} else {
alert('Failed to send dummy event.');
}
})
.catch(error => {
console.error('Error triggering dummy event:', error);
alert('Error sending dummy event.');
});
});
}
});
Production Considerations and Enhancements
This recipe provides a foundational structure. For production environments, consider the following:
- Persistent WebSocket Connection: The current PHP implementation relies on WP Cron, which is not a true persistent connection. For reliable real-time, use a PHP WebSocket library like Ratchet or Swoole, or run the Node.js server as a robust service (e.g., using PM2). The Node.js server should then directly communicate with your WordPress site via its REST API or a custom endpoint to trigger events, rather than relying on WP Cron.
- Security: Implement authentication and authorization for WebSocket connections. Ensure your WebSocket server is protected and accessible only to authorized clients. Use WSS (WebSocket Secure) for encrypted communication.
- Scalability: For high-traffic sites, consider a message broker (like Redis Pub/Sub, RabbitMQ, or Kafka) between your WordPress backend and the WebSocket server.
- Error Handling and Logging: Enhance error handling, especially around filesystem operations and network connections. Implement comprehensive logging on both the Node.js server and the WordPress plugin.
- State Management: The JSON file approach is simple but can become a bottleneck. For more complex state, consider using a database or a key-value store.
- Client Reconnection Logic: The JavaScript includes basic reconnection logic. Fine-tune the delay and retry attempts based on your application’s needs.
- WordPress REST API Integration: Instead of WP Cron, the Node.js server could make authenticated requests to a custom WordPress REST API endpoint to trigger events or update data, which then uses the Filesystem API.
Deployment Steps
- Install the Node.js WebSocket server (`npm install`).
- Start the Node.js WebSocket server (`node server.js`).
- Upload the `wp-realtime-events` plugin directory to your WordPress site’s `wp-content/plugins/` directory.
- Activate the “WP Realtime Events” plugin in the WordPress admin area.
- Ensure your frontend JavaScript is enqueued correctly (either via theme or plugin).
- Test by performing actions that trigger events (e.g., saving a post) and observe the console logs in your browser and the Node.js server output.
By combining the power of WebSockets for instant communication and the WordPress Filesystem API for persistent event state, you can build highly responsive and dynamic features within your WordPress applications.