WordPress Development Recipe: Real-time custom event triggers using WebSockets and REST API Controllers
Leveraging WebSockets for Real-time WordPress Event Notifications
For e-commerce platforms built on WordPress, delivering instant feedback to users and administrators is paramount. Imagine a customer completing a purchase, and the order status updating in real-time on an admin dashboard without a page refresh. Or a new support ticket appearing instantly for a customer service agent. This level of interactivity is typically achieved through WebSockets. This recipe outlines how to integrate a WebSocket server with WordPress, triggered by custom REST API events, to push real-time notifications.
Setting Up a Node.js WebSocket Server
We’ll use Node.js with the popular `ws` library for our WebSocket server. This server will act as a central hub, listening for events from WordPress and broadcasting them to connected clients.
First, initialize a Node.js project and install the necessary package:
mkdir wordpress-ws-server cd wordpress-ws-server npm init -y npm install ws express
Next, create the main server file, e.g., server.js:
const WebSocket = require('ws');
const express = require('express');
const http = require('http');
const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
const PORT = process.env.PORT || 8080;
// Store connected clients
const clients = new Map();
let clientIdCounter = 0;
wss.on('connection', (ws) => {
const id = clientIdCounter++;
clients.set(id, ws);
console.log(`Client connected: ${id}`);
ws.on('message', (message) => {
console.log(`Received message from client ${id}: ${message}`);
// Optionally, broadcast messages back to all clients or specific ones
// wss.clients.forEach((client) => {
// if (client !== ws && client.readyState === WebSocket.OPEN) {
// client.send(`Broadcast from ${id}: ${message}`);
// }
// });
});
ws.on('close', () => {
clients.delete(id);
console.log(`Client disconnected: ${id}`);
});
ws.on('error', (error) => {
console.error(`WebSocket error for client ${id}:`, error);
clients.delete(id);
});
// Send a welcome message to the newly connected client
ws.send(JSON.stringify({ type: 'welcome', message: 'Connected to WebSocket server!' }));
});
// Endpoint to receive events from WordPress
app.post('/event', express.json(), (req, res) => {
const eventData = req.body;
console.log('Received event from WordPress:', eventData);
// Broadcast the event to all connected clients
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(eventData));
}
});
res.status(200).json({ message: 'Event received and broadcasted' });
});
server.listen(PORT, () => {
console.log(`WebSocket server started on port ${PORT}`);
});
// Function to broadcast to specific client (example, not used by WP directly)
function broadcastToClient(clientId, message) {
if (clients.has(clientId)) {
const client = clients.get(clientId);
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
}
}
}
// Export for potential modularity or testing
module.exports = { server, wss, broadcastToClient };
To run this server, execute:
node server.js
This server listens on port 8080 and exposes a POST /event endpoint. When WordPress sends data to this endpoint, the server will broadcast it to all connected WebSocket clients.
WordPress REST API Controller for Event Publishing
We need a mechanism within WordPress to trigger events and send them to our WebSocket server. This can be achieved by creating a custom REST API endpoint that acts as a publisher. When this endpoint is called, it will format the data and send an HTTP POST request to our Node.js WebSocket server.
Create a new plugin or add this code to your theme’s functions.php (though a plugin is highly recommended for maintainability).
<?php
/**
* Plugin Name: Real-time Event Publisher
* Description: Publishes custom events to a WebSocket server.
* Version: 1.0
* Author: Your Name
*/
// Define WebSocket server URL
define('MY_WS_SERVER_URL', 'http://localhost:8080/event'); // Replace with your server's actual URL
/**
* Register a custom REST API endpoint to trigger events.
*/
add_action('rest_api_init', function () {
register_rest_route('myevents/v1', '/publish', array(
'methods' => 'POST',
'callback' => 'my_publish_event_callback',
'permission_callback' => function () {
// Implement proper authentication/authorization here.
// For simplicity, allowing any authenticated user.
return current_user_can('edit_posts');
}
));
});
/**
* Callback function for the REST API endpoint.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
function my_publish_event_callback(WP_REST_Request $request) {
$event_data = $request->get_json_params();
if (empty($event_data)) {
return new WP_Error('invalid_data', 'No event data provided.', array('status' => 400));
}
// Ensure a 'type' is present for clarity
if (!isset($event_data['type'])) {
$event_data['type'] = 'generic_event'; // Default type
}
// Add a timestamp
$event_data['timestamp'] = current_time('mysql');
// Send the event data to the WebSocket server
$response = wp_remote_post(MY_WS_SERVER_URL, array(
'method' => 'POST',
'timeout' => 45,
'redirection' => 5,
'httpversion' => '1.0',
'body' => json_encode($event_data),
'headers' => array(
'Content-Type' => 'application/json',
),
));
if (is_wp_error($response)) {
$error_message = $response->get_error_message();
error_log("Error sending event to WS server: " . $error_message);
return new WP_Error('ws_server_error', 'Failed to send event to WebSocket server: ' . $error_message, array('status' => 500));
}
$response_code = wp_remote_retrieve_response_code($response);
$response_body = wp_remote_retrieve_body($response);
if ($response_code !== 200) {
error_log("WS server returned error: " . $response_code . " - " . $response_body);
return new WP_Error('ws_server_error', 'WebSocket server responded with an error.', array('status' => $response_code));
}
// Success
return new WP_REST_Response(array(
'message' => 'Event published successfully',
'data_sent' => $event_data
), 200);
}
/**
* Example: Triggering an event on order completion (requires WooCommerce).
* This is a conceptual example. You'd hook into WooCommerce's specific actions.
*/
add_action('woocommerce_order_status_changed', function ($order_id, $old_status, $new_status) {
if ('completed' === $new_status) {
$order = wc_get_order($order_id);
if ($order) {
$event_payload = array(
'type' => 'order_completed',
'order_id' => $order_id,
'customer_email' => $order->get_billing_email(),
'total' => $order->get_total(),
'currency' => $order->get_currency(),
'status' => $new_status,
);
// Use wp_remote_post to send to our custom REST endpoint,
// which then forwards to the WS server.
// Alternatively, you could directly call the WS server here if preferred,
// but using the REST endpoint provides a unified publishing mechanism.
$publish_endpoint = rest_url('myevents/v1/publish');
$response = wp_remote_post($publish_endpoint, array(
'method' => 'POST',
'timeout' => 45,
'body' => json_encode($event_payload),
'headers' => array(
'Content-Type' => 'application/json',
'Authorization' => 'Bearer YOUR_API_KEY_OR_NONCE' // If your publish endpoint requires auth
),
));
if (is_wp_error($response)) {
error_log("Error triggering order_completed event via REST: " . $response->get_error_message());
} else {
$response_code = wp_remote_retrieve_response_code($response);
if ($response_code !== 200) {
error_log("REST endpoint for order_completed event returned error: " . $response_code);
} else {
// Event successfully queued for publishing
error_log("Order completed event queued for publishing: " . $order_id);
}
}
}
}
}, 10, 3);
// Basic authentication for the publish endpoint (example)
// In a real-world scenario, use nonces, JWT, or application passwords.
add_filter('rest_authentication_errors', function ($result) {
// If a previous authentication check has failed, return that.
if (true !== $result && is_wp_error($result)) {
return $result;
}
// If the user is logged in, we don't need to do anything.
if (get_current_user_id()) {
return $result;
}
// If no user is logged in, and the request is for our specific endpoint,
// we might want to allow it if it has a valid token/key.
// For this example, we'll just check if the request is for our publish endpoint.
// A more robust solution would involve checking headers for an API key.
$request = \WP_REST_Server::get_instance()->get_request();
if (isset($request->get_route()[0]) && $request->get_route()[0] === '/myevents/v1/publish') {
// Here you would implement your API key validation.
// Example:
// $headers = $request->get_headers();
// if (!isset($headers['authorization']) || $headers['authorization'] !== 'Bearer YOUR_SECURE_KEY') {
// return new WP_Error('rest_forbidden', __('Invalid API key.'), array('status' => 401));
// }
// For now, let's assume it's allowed if it matches the route, but this is INSECURE for production.
// return $result; // Allow if authenticated or if we implement key check
}
// Otherwise, return the result of the default authentication.
return $result;
});
Important Security Note: The permission_callback and the example rest_authentication_errors filter are placeholders. In a production environment, you MUST implement robust authentication and authorization for your /publish endpoint. This could involve API keys, OAuth tokens, or ensuring the user has specific capabilities.
Client-Side JavaScript for WebSocket Connection
On the front-end (e.g., in a theme’s JavaScript file or a dedicated plugin), you’ll need JavaScript to establish a WebSocket connection to your Node.js server and listen for incoming messages.
document.addEventListener('DOMContentLoaded', () => {
const wsUrl = 'ws://localhost:8080'; // Replace with your WebSocket server URL
let socket;
function connectWebSocket() {
socket = new WebSocket(wsUrl);
socket.onopen = () => {
console.log('WebSocket connection established.');
// Send a message to the server upon connection if needed
// socket.send(JSON.stringify({ type: 'identify', userId: 'some_user_id' }));
};
socket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log('Message from server:', data);
// Handle different event types
switch (data.type) {
case 'welcome':
console.log('Server welcome message:', data.message);
break;
case 'order_completed':
console.log(`New order completed: #${data.order_id}`);
// Update UI, show notification, etc.
displayNotification(`Order #${data.order_id} completed! Total: ${data.total}`);
break;
case 'new_support_ticket':
console.log('New support ticket received:', data.ticket_id);
// Update support dashboard
break;
// Add more cases for other event types
default:
console.log('Received unknown event type:', data.type);
}
} catch (e) {
console.error('Failed to parse message or handle event:', e);
}
};
socket.onclose = (event) => {
console.log('WebSocket connection closed:', event.code, event.reason);
// Attempt to reconnect after a delay
setTimeout(connectWebSocket, 5000); // Reconnect every 5 seconds
};
socket.onerror = (error) => {
console.error('WebSocket error:', error);
// The 'onclose' event will typically follow an error
};
}
function displayNotification(message) {
// Example: Append to a notification area on the page
const notificationArea = document.getElementById('ws-notifications');
if (!notificationArea) {
const div = document.createElement('div');
div.id = 'ws-notifications';
div.style.position = 'fixed';
div.style.bottom = '20px';
div.style.right = '20px';
div.style.backgroundColor = '#4CAF50';
div.style.color = 'white';
div.style.padding = '15px';
div.style.borderRadius = '5px';
div.style.zIndex = '1000';
document.body.appendChild(div);
notificationArea = div;
}
const p = document.createElement('p');
p.textContent = message;
notificationArea.appendChild(p);
// Remove notification after some time
setTimeout(() => {
notificationArea.removeChild(p);
if (notificationArea.children.length === 0) {
notificationArea.remove();
}
}, 10000); // 10 seconds
}
// Initial connection
connectWebSocket();
});
To integrate this JavaScript, enqueue it properly in WordPress. For example, in your plugin file:
function enqueue_ws_client_script() {
// Ensure this path is correct relative to your plugin's root directory
wp_enqueue_script('ws-client', plugin_dir_url(__FILE__) . 'js/ws-client.js', array(), '1.0', true);
}
add_action('wp_enqueue_scripts', 'enqueue_ws_client_script');
// Use 'admin_enqueue_scripts' for admin-only functionality
Triggering Events Programmatically
Beyond specific hooks like WooCommerce order status changes, you can trigger events from anywhere in your WordPress code. The most straightforward way is to directly call the custom REST API endpoint you created.
function trigger_custom_event($event_type, $event_data = array()) {
$publish_endpoint = rest_url('myevents/v1/publish');
$payload = array_merge(array(
'type' => $event_type,
'timestamp' => current_time('mysql'),
), $event_data);
$response = wp_remote_post($publish_endpoint, array(
'method' => 'POST',
'timeout' => 45,
'body' => json_encode($payload),
'headers' => array(
'Content-Type' => 'application/json',
// Add authentication headers if your publish endpoint requires them
// 'Authorization' => 'Bearer YOUR_API_KEY'
),
));
if (is_wp_error($response)) {
error_log("Error triggering custom event '{$event_type}': " . $response->get_error_message());
return false;
}
$response_code = wp_remote_retrieve_response_code($response);
if ($response_code !== 200) {
error_log("Custom event '{$event_type}' endpoint returned error: " . $response_code);
return false;
}
return true;
}
// Example usage:
// trigger_custom_event('user_registered', array('user_id' => 123, 'username' => 'john_doe'));
// trigger_custom_event('product_low_stock', array('product_id' => 456, 'stock_level' => 5));
Production Considerations
For a production environment, several aspects require careful attention:
- Scalability: The Node.js WebSocket server might need to be scaled horizontally. Consider using a message broker like Redis Pub/Sub or RabbitMQ to decouple the Node.js server instances and allow them to broadcast messages reliably across all instances. Your WordPress plugin would publish to Redis, and each Node.js instance would subscribe to Redis and broadcast to its connected clients.
- Security: Implement robust authentication for both the WordPress REST API endpoint publishing events and potentially for WebSocket connections themselves (e.g., using JWTs passed during connection). Ensure TLS/SSL is used for both HTTP and WebSocket (WSS) connections.
- Error Handling & Resilience: Implement comprehensive error logging on both the Node.js server and the WordPress plugin. The client-side JavaScript should have robust reconnection logic.
- Deployment: Use a process manager like PM2 for the Node.js server to ensure it stays running and restarts automatically on crashes.
- Configuration Management: Externalize sensitive information like server URLs and API keys using environment variables.
- Client Identification: For more advanced scenarios, you might want to identify specific connected clients (e.g., by user ID) and send targeted messages rather than broadcasting to everyone. This requires passing user identifiers during the WebSocket connection and storing them alongside the connection object in the `clients` map.
By combining WordPress’s REST API capabilities with a dedicated WebSocket server, you can build highly interactive and real-time features for your e-commerce site, enhancing user experience and operational efficiency.