Skip to content

Instantly share code, notes, and snippets.

@scrubmx
Created July 24, 2025 23:43
Show Gist options
  • Save scrubmx/db685f091ce9104b0f0092ef2f4b3a03 to your computer and use it in GitHub Desktop.
Save scrubmx/db685f091ce9104b0f0092ef2f4b3a03 to your computer and use it in GitHub Desktop.
WordPress Wehook Request When Post is Published
<?php
/**
* Utility class for handling web-hook requests.
* It provides a method to send POST requests to a specified URL with retries and error handling.
*/
class WP_Webhook_Request
{
protected const RETRY_COUNT_LIMIT = 3;
protected const RETRY_DELAY_SECONDS = 2;
protected const RETRY_MAX_DELAY_SECONDS = 8;
protected const REQUEST_TIMEOUT_SECONDS = 5;
private const WEBHOOK_ENDPOINT_URL = 'https://example.com/webhook';
private const WEBHOOK_SIGNATURE_SECRET = SECURE_AUTH_KEY;
/**
* Posts a web-hook request with the given post data.
* It encodes the post data as JSON, sets the necessary headers, and retries the request if it fails.
*/
public static function post($post)
{
$json = wp_json_encode([
'event' => 'post_published',
'data' => [
'post' => [
'id' => $post->ID,
'type' => $post->post_type,
'status' => $post->post_status,
'title' => get_the_title($post),
'excerpt' => get_the_excerpt($post),
'permalink' => get_permalink($post),
'published_at' => get_post_time('c', true, $post),
'images' => [
'thumbnail' => get_the_post_thumbnail_url($post, 'thumbnail'),
'medium' => get_the_post_thumbnail_url($post, 'medium'),
'large' => get_the_post_thumbnail_url($post, 'large'),
'full' => get_the_post_thumbnail_url($post, 'full'),
],
],
],
]);
if (! $json) {
error_log('Webhook Error: Failed to encode data');
return false;
}
$idempotency_key = hash('sha256', $post->ID);
return static::postWithRetries($json, $idempotency_key);
}
/**
* Posts a web-hook request with retries.
* It sends a POST request to the web-hook URL with the provided JSON data and idempotency key.
* If the request fails, it retries up to a specified limit with exponential back-off.
*
* @param string $json The JSON-encoded data to be sent in the request body.
* @param string $idempotency_key The idempotency key to be used in the request headers.
* @param integer $attempt_count The current attempt count for retries.
* @return boolean True if the request was successful, false otherwise.
*/
protected static function postWithRetries($json, $idempotency_key, $attempt_count = 0)
{
$success = false;
$response = wp_remote_post(static::WEBHOOK_ENDPOINT_URL, [
'body' => $json,
'data_format' => 'body',
'timeout' => static::REQUEST_TIMEOUT_SECONDS,
'headers' => [
'Content-Type' => 'application/json',
'X-Webhook-Event' => 'post_published',
'X-Webhook-Timestamp' => gmdate('c'),
'X-Idempotency-Key' => $idempotency_key,
'X-Signature' => hash_hmac('sha256', $json, static::WEBHOOK_SIGNATURE_SECRET),
],
]);
if (! is_wp_error($response)) {
$code = (int) wp_remote_retrieve_response_code($response);
$success = (bool) $code >= 200 && $code < 300;
}
// If the request was successful, we're done
if ($success) return true;
// If we haven't reached the maximum number of retries, retry the request
if ($attempt_count < static::RETRY_COUNT_LIMIT) {
$attempt_count += 1;
// Sleep with exponential back off and a maximum delay of 8 seconds
sleep(
min(pow(static::RETRY_DELAY_SECONDS, $attempt_count), static::RETRY_MAX_DELAY_SECONDS)
);
return static::postWithRetries($json, $idempotency_key, $attempt_count);
}
// If we reach here, all attempts failed
if (is_wp_error($response)) {
error_log('Webhook Error (after ' . static::RETRY_COUNT_LIMIT . ' retries): ' . $response->get_error_message());
} else {
$code = is_array($response) ? wp_remote_retrieve_response_code($response) : 'N/A';
$body = is_array($response) ? wp_remote_retrieve_body($response) : '';
error_log('Webhook Error (after ' . static::RETRY_COUNT_LIMIT . " retries):\r\nHTTP Status Code: $code\r\nResponse Body: $body");
}
return false;
}
}
/**
* Custom post type for Anuncios (Notices).
*/
class WP_Anuncios
{
public const SLUG = 'anuncios';
public const INFO_MESSAGE = "Cuando publiques un Anuncio, todos los usuarios recibirán un correo electrónico notificándoles sobre el nuevo anuncio.";
/**
* Initialize the Anuncios post type and its related functionality.
*/
public static function init()
{
add_theme_support('post-thumbnails');
WP_Anuncios::actionRegisterPostType();
WP_Anuncios::actionDisableComments();
WP_Anuncios::actionAdminInfoMessage();
WP_Anuncios::actionBlockEditorInfoMessage();
WP_Anuncios::actionPublishAnuncios();
WP_Anuncios::actionLoadTranslations();
}
/**
* Check if the current screen is the Anuncios post type.
*/
public static function isActiveScreen()
{
/** @var WP_Screen|null $screen */
$screen = get_current_screen();
return isset($screen->post_type) && $screen->post_type === WP_Anuncios::SLUG;
}
/**
* Check if the given post is of the Anuncios post type.
*/
public static function isPostType($post)
{
return isset($post->post_type) && $post->post_type === WP_Anuncios::SLUG;
}
/*
|--------------------------------------------------------------------------
| WordPress Register Post Type & Disable Comments
|--------------------------------------------------------------------------
*/
/**
* Create custom post type for notices.
*/
public static function actionRegisterPostType()
{
/**
* Create custom post type for notices.
*/
add_action('init', function () {
register_post_type(WP_Anuncios::SLUG, [
'public' => true,
'show_in_menu' => true,
'has_archive' => true,
'show_in_rest' => true,
'publicly_queryable' => true,
'capability_type' => 'post',
'menu_icon' => 'dashicons-megaphone',
'rewrite' => ['slug' => WP_Anuncios::SLUG],
'supports' => ['title', 'editor', 'thumbnail', 'excerpt', 'custom-fields'],
'labels' => [
'name' => _x('Anuncios', 'Post type general name', 'zafiro-child'),
'singular_name' => _x('Anuncio', 'Post type singular name', 'zafiro-child'),
'menu_name' => _x('Anuncios', 'Admin Menu text', 'zafiro-child'),
'name_admin_bar' => _x('Anuncio', 'Add New on Toolbar', 'zafiro-child'),
'add_new' => __('Agregar nuevo', 'zafiro-child'),
'add_new_item' => __('Agregar nuevo anuncio', 'zafiro-child'),
'new_item' => __('Nuevo anuncio', 'zafiro-child'),
'edit_item' => __('Editar anuncio', 'zafiro-child'),
'view_item' => __('Ver anuncio', 'zafiro-child'),
'all_items' => __('Todos los anuncios', 'zafiro-child'),
'search_items' => __('Buscar anuncios', 'zafiro-child'),
'not_found' => __('No se encontraron anuncios.', 'zafiro-child'),
'not_found_in_trash' => __('No hay anuncios en la papelera.', 'zafiro-child'),
'featured_image' => __('Imagen destacada', 'zafiro-child'),
'set_featured_image' => __('Establecer imagen destacada', 'zafiro-child'),
'remove_featured_image' => __('Eliminar imagen destacada', 'zafiro-child'),
'use_featured_image' => __('Usar como imagen destacada', 'zafiro-child'),
'archives' => __('Archivo de anuncios', 'zafiro-child'),
'insert_into_item' => __('Insertar en el anuncio', 'zafiro-child'),
'uploaded_to_this_item' => __('Subido a este anuncio', 'zafiro-child'),
'filter_items_list' => __('Filtrar lista de anuncios', 'zafiro-child'),
'items_list_navigation' => __('Navegación de la lista de anuncios', 'zafiro-child'),
'items_list' => __('Lista de anuncios', 'zafiro-child'),
],
]);
});
}
/**
* Disable comments for the Anuncios post type.
* This function modifies the comments behavior, hides the comments meta-box in the admin area,
* and removes the comments column from the admin index for the Anuncios post type.
*/
public static function actionDisableComments()
{
// Explicitly close comments for the post type
add_filter('comments_open', function ($open, $post_id) {
/** @var WP_Post|null $post */
$post = get_post($post_id);
return WP_Anuncios::isPostType($post) ? false : $open;
}, 10, 2);
add_filter('pings_open', function ($open, $post_id) {
/** @var WP_Post|null $post */
$post = get_post($post_id);
return WP_Anuncios::isPostType($post) ? false : $open;
}, 10, 2);
// Hide comments meta-box for the post type
add_action('admin_menu', function () {
remove_meta_box('commentsdiv', WP_Anuncios::SLUG, 'normal');
remove_meta_box('commentstatusdiv', WP_Anuncios::SLUG, 'normal');
});
// Hide comments column in admin index for the post type
add_filter('manage_' . WP_Anuncios::SLUG . '_posts_columns', function ($columns) {
if (isset($columns['comments'])) {
unset($columns['comments']);
}
return $columns;
});
}
/*
|--------------------------------------------------------------------------
| WordPress Display Info Message to Admins
|--------------------------------------------------------------------------
*/
/**
* Add custom info notice on classic admin pages.
*/
public static function actionAdminInfoMessage()
{
add_action('admin_notices', function () {
if (! WP_Anuncios::isActiveScreen()) return;
$message = esc_html(WP_Anuncios::INFO_MESSAGE);
echo <<<HTML
<div class="notice notice-info"><p><strong>Importante:</strong> $message</p></div>
HTML;
});
}
/**
* Add custom info notice on block editor admin pages.
*/
public static function actionBlockEditorInfoMessage()
{
add_action('enqueue_block_editor_assets', function () {
wp_enqueue_script('wp-notices');
if (! WP_Anuncios::isActiveScreen()) return;
$message = esc_js(strip_tags(WP_Anuncios::INFO_MESSAGE));
wp_add_inline_script('wp-notices', <<<JS
window.wp && wp.domReady && wp.domReady(function() {
wp.data
.dispatch('core/notices')
.createNotice('info', 'Importante: $message', { isDismissible: false });
});
JS);
});
}
/*
|--------------------------------------------------------------------------
| WordPress Bootstrap Web-Hook to Send Emails on Post Publish
|--------------------------------------------------------------------------
*/
/**
* Call email web-hook when a new post is published.
*/
public static function actionPublishAnuncios()
{
add_action('publish_' . WP_Anuncios::SLUG, function ($post_id) {
/** @var WP_Post|null $post */
$post = get_post($post_id);
if (! isset($post->post_status) || $post->post_status !== 'publish') return;
WP_Webhook_Request::post($post);
});
}
/*
|--------------------------------------------------------------------------
| WordPress Add Translations for the Anuncios Post Type
|--------------------------------------------------------------------------
*/
/**
* Load translations for the Anuncios post type.
*/
public static function actionLoadTranslations()
{
add_action('after_setup_theme', function () {
load_theme_textdomain('zafiro-child', get_stylesheet_directory() . '/languages');
});
}
}
WP_Anuncios::init();
@scrubmx
Copy link
Author

scrubmx commented Jul 24, 2025

This implements a custom post-type
The webhook request has a naive implementation of retries with exponential backoff
The headers include a signature for validation and idempotency key enable handling duplicated requests

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment