hook-builder.php

I’ll build this as a full WordPress theme “hook builder” module: admin UI, saved hook locations, priorities, conditional display, shortcode/PHP-safe callbacks, import/export, and frontend rendering.

Absolutely, my dear friend. Below is an advanced inc/builders/hook-builder.php file for your Rx Theme. It adds a powerful hook builder system with:

  • Admin panel under Appearance → RX Hook Builder
  • Add unlimited hook blocks
  • Enable/disable each block
  • Select WordPress/theme hook location
  • Priority control
  • HTML / shortcode / template-part output
  • Device targeting: desktop, tablet, mobile
  • Login targeting: everyone, logged-in, logged-out
  • User role targeting
  • Page/post/category/archive/search/home/404 conditions
  • Date start/end scheduling
  • Import/export JSON
  • Safe sanitization
  • Frontend rendering
  • Shortcode support
  • Manual theme hook output support

1. Create this file

Path:

inc/builders/hook-builder.php

Paste this full code:

<?php
/**
 * RX Theme Hook Builder
 *
 * File: inc/builders/hook-builder.php
 *
 * Advanced hook builder for RX Theme.
 * Allows adding custom content into WordPress/theme hooks with conditions.
 *
 * @package RX_Theme
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'RX_Theme_Hook_Builder' ) ) :

final class RX_Theme_Hook_Builder {

	/**
	 * Option name.
	 *
	 * @var string
	 */
	const OPTION_NAME = 'rx_theme_hook_builder_items';

	/**
	 * Nonce action.
	 *
	 * @var string
	 */
	const NONCE_ACTION = 'rx_theme_hook_builder_action';

	/**
	 * Nonce name.
	 *
	 * @var string
	 */
	const NONCE_NAME = 'rx_theme_hook_builder_nonce';

	/**
	 * Admin page slug.
	 *
	 * @var string
	 */
	const PAGE_SLUG = 'rx-theme-hook-builder';

	/**
	 * Singleton instance.
	 *
	 * @var self|null
	 */
	private static $instance = null;

	/**
	 * Get instance.
	 *
	 * @return self
	 */
	public static function instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}

		return self::$instance;
	}

	/**
	 * Constructor.
	 */
	private function __construct() {
		add_action( 'init', array( $this, 'register_frontend_hooks' ), 20 );
		add_action( 'admin_menu', array( $this, 'register_admin_page' ) );
		add_action( 'admin_init', array( $this, 'handle_admin_actions' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'admin_assets' ) );

		add_shortcode( 'rx_hook_area', array( $this, 'shortcode_hook_area' ) );
		add_shortcode( 'rx_hook_block', array( $this, 'shortcode_hook_block' ) );
	}

	/**
	 * Get default hook locations.
	 *
	 * You can add your own theme hooks here.
	 *
	 * @return array
	 */
	public function get_hook_locations() {
		$hooks = array(
			'wp_head'                  => 'WordPress: wp_head',
			'wp_body_open'             => 'WordPress: wp_body_open',
			'wp_footer'                => 'WordPress: wp_footer',

			'loop_start'               => 'WordPress: loop_start',
			'loop_end'                 => 'WordPress: loop_end',
			'the_content_before'       => 'RX Virtual: Before Post Content',
			'the_content_after'        => 'RX Virtual: After Post Content',

			'rx_before_site'           => 'RX Theme: Before Site',
			'rx_after_site'            => 'RX Theme: After Site',

			'rx_before_header'         => 'RX Theme: Before Header',
			'rx_header'                => 'RX Theme: Header',
			'rx_after_header'          => 'RX Theme: After Header',

			'rx_before_nav'            => 'RX Theme: Before Navigation',
			'rx_after_nav'             => 'RX Theme: After Navigation',

			'rx_before_main'           => 'RX Theme: Before Main',
			'rx_after_main'            => 'RX Theme: After Main',

			'rx_before_content'        => 'RX Theme: Before Content',
			'rx_after_content'         => 'RX Theme: After Content',

			'rx_before_sidebar'        => 'RX Theme: Before Sidebar',
			'rx_after_sidebar'         => 'RX Theme: After Sidebar',

			'rx_before_footer'         => 'RX Theme: Before Footer',
			'rx_footer'                => 'RX Theme: Footer',
			'rx_after_footer'          => 'RX Theme: After Footer',

			'rx_before_single'         => 'RX Theme: Before Single Post',
			'rx_after_single'          => 'RX Theme: After Single Post',
			'rx_before_page'           => 'RX Theme: Before Page',
			'rx_after_page'            => 'RX Theme: After Page',
			'rx_before_archive'        => 'RX Theme: Before Archive',
			'rx_after_archive'         => 'RX Theme: After Archive',
			'rx_before_search'         => 'RX Theme: Before Search',
			'rx_after_search'          => 'RX Theme: After Search',
			'rx_before_404'            => 'RX Theme: Before 404',
			'rx_after_404'             => 'RX Theme: After 404',

			'rx_inside_article_top'    => 'RX Theme: Inside Article Top',
			'rx_inside_article_bottom' => 'RX Theme: Inside Article Bottom',

			'rx_before_comments'       => 'RX Theme: Before Comments',
			'rx_after_comments'        => 'RX Theme: After Comments',
		);

		return apply_filters( 'rx_theme_hook_builder_locations', $hooks );
	}

	/**
	 * Register frontend actions dynamically.
	 *
	 * @return void
	 */
	public function register_frontend_hooks() {
		$items = $this->get_items();

		if ( empty( $items ) ) {
			return;
		}

		foreach ( $items as $item ) {
			if ( empty( $item['status'] ) || 'enabled' !== $item['status'] ) {
				continue;
			}

			$hook     = ! empty( $item['hook'] ) ? sanitize_key( $item['hook'] ) : '';
			$priority = isset( $item['priority'] ) ? absint( $item['priority'] ) : 10;

			if ( empty( $hook ) ) {
				continue;
			}

			if ( 'the_content_before' === $hook || 'the_content_after' === $hook ) {
				add_filter( 'the_content', array( $this, 'filter_the_content' ), $priority );
				continue;
			}

			add_action(
				$hook,
				function () use ( $item ) {
					$this->render_item( $item );
				},
				$priority
			);
		}
	}

	/**
	 * Add virtual content hooks around the_content.
	 *
	 * @param string $content Content.
	 * @return string
	 */
	public function filter_the_content( $content ) {
		if ( is_admin() || ! in_the_loop() || ! is_main_query() ) {
			return $content;
		}

		$before = '';
		$after  = '';
		$items  = $this->get_items();

		foreach ( $items as $item ) {
			if ( empty( $item['status'] ) || 'enabled' !== $item['status'] ) {
				continue;
			}

			if ( empty( $item['hook'] ) ) {
				continue;
			}

			if ( ! $this->item_can_display( $item ) ) {
				continue;
			}

			ob_start();
			$this->render_item( $item, false );
			$output = ob_get_clean();

			if ( 'the_content_before' === $item['hook'] ) {
				$before .= $output;
			}

			if ( 'the_content_after' === $item['hook'] ) {
				$after .= $output;
			}
		}

		return $before . $content . $after;
	}

	/**
	 * Register admin page.
	 *
	 * @return void
	 */
	public function register_admin_page() {
		add_theme_page(
			esc_html__( 'RX Hook Builder', 'rx-theme' ),
			esc_html__( 'RX Hook Builder', 'rx-theme' ),
			'manage_options',
			self::PAGE_SLUG,
			array( $this, 'render_admin_page' )
		);
	}

	/**
	 * Admin assets.
	 *
	 * @param string $hook Current admin hook.
	 * @return void
	 */
	public function admin_assets( $hook ) {
		if ( false === strpos( $hook, self::PAGE_SLUG ) ) {
			return;
		}

		wp_enqueue_style( 'wp-color-picker' );
		wp_enqueue_script( 'wp-color-picker' );

		$css = '
			.rx-hook-wrap {
				max-width: 1250px;
			}
			.rx-hook-card {
				background: #fff;
				border: 1px solid #dcdcde;
				border-radius: 8px;
				padding: 18px;
				margin: 18px 0;
				box-shadow: 0 1px 2px rgba(0,0,0,.04);
			}
			.rx-hook-grid {
				display: grid;
				grid-template-columns: repeat(2, minmax(0, 1fr));
				gap: 16px;
			}
			.rx-hook-grid-3 {
				display: grid;
				grid-template-columns: repeat(3, minmax(0, 1fr));
				gap: 16px;
			}
			.rx-hook-card label {
				font-weight: 600;
				display: block;
				margin-bottom: 6px;
			}
			.rx-hook-card input[type="text"],
			.rx-hook-card input[type="number"],
			.rx-hook-card input[type="date"],
			.rx-hook-card select,
			.rx-hook-card textarea {
				width: 100%;
				max-width: 100%;
			}
			.rx-hook-card textarea {
				min-height: 160px;
				font-family: Consolas, Monaco, monospace;
			}
			.rx-hook-mini {
				color: #646970;
				font-size: 12px;
			}
			.rx-hook-badge {
				display: inline-block;
				padding: 3px 8px;
				background: #f0f6fc;
				border: 1px solid #c5d9ed;
				border-radius: 999px;
				font-size: 12px;
			}
			.rx-hook-danger {
				color: #b32d2e;
			}
			.rx-hook-actions {
				display: flex;
				gap: 8px;
				align-items: center;
				flex-wrap: wrap;
			}
			@media(max-width: 782px) {
				.rx-hook-grid,
				.rx-hook-grid-3 {
					grid-template-columns: 1fr;
				}
			}
		';

		wp_add_inline_style( 'wp-admin', $css );

		$js = '
			jQuery(function($){
				$(".rx-hook-confirm").on("click", function(e){
					if(!confirm("Are you sure?")) {
						e.preventDefault();
					}
				});

				$(".rx-hook-copy").on("click", function(e){
					e.preventDefault();
					var target = $(this).data("target");
					var text = $(target).val();
					if(navigator.clipboard){
						navigator.clipboard.writeText(text);
						alert("Copied!");
					}
				});
			});
		';

		wp_add_inline_script( 'jquery-core', $js );
	}

	/**
	 * Render admin page.
	 *
	 * @return void
	 */
	public function render_admin_page() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		$items          = $this->get_items();
		$editing_id     = isset( $_GET['edit'] ) ? sanitize_text_field( wp_unslash( $_GET['edit'] ) ) : '';
		$editing_item   = $editing_id ? $this->get_item( $editing_id ) : array();
		$hook_locations = $this->get_hook_locations();
		$roles          = wp_roles()->roles;

		$export_json = wp_json_encode( $items, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
		?>
		<div class="wrap rx-hook-wrap">
			<h1><?php esc_html_e( 'RX Theme Hook Builder', 'rx-theme' ); ?></h1>

			<p>
				<?php esc_html_e( 'Create reusable hook blocks and display them in theme or WordPress hook locations with conditions.', 'rx-theme' ); ?>
			</p>

			<?php $this->admin_notices(); ?>

			<div class="rx-hook-card">
				<h2>
					<?php
					echo $editing_item
						? esc_html__( 'Edit Hook Block', 'rx-theme' )
						: esc_html__( 'Add New Hook Block', 'rx-theme' );
					?>
				</h2>

				<form method="post" action="">
					<?php wp_nonce_field( self::NONCE_ACTION, self::NONCE_NAME ); ?>
					<input type="hidden" name="rx_hook_builder_action" value="save_item">
					<input type="hidden" name="item_id" value="<?php echo esc_attr( $editing_item['id'] ?? '' ); ?>">

					<div class="rx-hook-grid">
						<p>
							<label for="rx_title"><?php esc_html_e( 'Title', 'rx-theme' ); ?></label>
							<input type="text" id="rx_title" name="title" value="<?php echo esc_attr( $editing_item['title'] ?? '' ); ?>" placeholder="Header ad, Footer CTA, schema script...">
						</p>

						<p>
							<label for="rx_status"><?php esc_html_e( 'Status', 'rx-theme' ); ?></label>
							<select id="rx_status" name="status">
								<?php
								$status = $editing_item['status'] ?? 'enabled';
								$this->option( 'enabled', 'Enabled', $status );
								$this->option( 'disabled', 'Disabled', $status );
								?>
							</select>
						</p>
					</div>

					<div class="rx-hook-grid-3">
						<p>
							<label for="rx_hook"><?php esc_html_e( 'Hook Location', 'rx-theme' ); ?></label>
							<select id="rx_hook" name="hook">
								<?php
								$current_hook = $editing_item['hook'] ?? 'wp_footer';

								foreach ( $hook_locations as $hook_key => $hook_label ) {
									printf(
										'<option value="%s" %s>%s</option>',
										esc_attr( $hook_key ),
										selected( $current_hook, $hook_key, false ),
										esc_html( $hook_label )
									);
								}
								?>
							</select>
							<span class="rx-hook-mini">
								<?php esc_html_e( 'Theme hooks must exist in your template files using do_action().', 'rx-theme' ); ?>
							</span>
						</p>

						<p>
							<label for="rx_priority"><?php esc_html_e( 'Priority', 'rx-theme' ); ?></label>
							<input type="number" id="rx_priority" name="priority" value="<?php echo esc_attr( $editing_item['priority'] ?? 10 ); ?>" min="0" max="999">
						</p>

						<p>
							<label for="rx_content_type"><?php esc_html_e( 'Content Type', 'rx-theme' ); ?></label>
							<select id="rx_content_type" name="content_type">
								<?php
								$content_type = $editing_item['content_type'] ?? 'html';
								$this->option( 'html', 'HTML / Text', $content_type );
								$this->option( 'shortcode', 'Shortcode Content', $content_type );
								$this->option( 'template_part', 'Template Part', $content_type );
								?>
							</select>
						</p>
					</div>

					<p>
						<label for="rx_content"><?php esc_html_e( 'Content', 'rx-theme' ); ?></label>
						<textarea id="rx_content" name="content" placeholder="HTML, shortcode, or template path like template-parts/hooks/header-ad"><?php echo esc_textarea( $editing_item['content'] ?? '' ); ?></textarea>
						<span class="rx-hook-mini">
							<?php esc_html_e( 'For template part, write path without .php. Example: template-parts/hooks/header-ad', 'rx-theme' ); ?>
						</span>
					</p>

					<h3><?php esc_html_e( 'Display Conditions', 'rx-theme' ); ?></h3>

					<div class="rx-hook-grid-3">
						<p>
							<label for="rx_condition_mode"><?php esc_html_e( 'Condition Mode', 'rx-theme' ); ?></label>
							<select id="rx_condition_mode" name="condition_mode">
								<?php
								$condition_mode = $editing_item['condition_mode'] ?? 'all';
								$this->option( 'all', 'Show Everywhere', $condition_mode );
								$this->option( 'include', 'Show Only On Selected Conditions', $condition_mode );
								$this->option( 'exclude', 'Hide On Selected Conditions', $condition_mode );
								?>
							</select>
						</p>

						<p>
							<label for="rx_logged_in"><?php esc_html_e( 'Login Visibility', 'rx-theme' ); ?></label>
							<select id="rx_logged_in" name="logged_in">
								<?php
								$logged_in = $editing_item['logged_in'] ?? 'all';
								$this->option( 'all', 'Everyone', $logged_in );
								$this->option( 'logged_in', 'Logged-in Users Only', $logged_in );
								$this->option( 'logged_out', 'Logged-out Visitors Only', $logged_in );
								?>
							</select>
						</p>

						<p>
							<label for="rx_device"><?php esc_html_e( 'Device Visibility', 'rx-theme' ); ?></label>
							<select id="rx_device" name="device">
								<?php
								$device = $editing_item['device'] ?? 'all';
								$this->option( 'all', 'All Devices', $device );
								$this->option( 'desktop', 'Desktop Only', $device );
								$this->option( 'mobile', 'Mobile / Tablet Only', $device );
								?>
							</select>
						</p>
					</div>

					<div class="rx-hook-grid">
						<p>
							<label for="rx_conditions"><?php esc_html_e( 'Conditions', 'rx-theme' ); ?></label>
							<select id="rx_conditions" name="conditions[]" multiple size="12">
								<?php
								$current_conditions = $editing_item['conditions'] ?? array();

								$condition_options = $this->get_condition_options();

								foreach ( $condition_options as $condition_key => $condition_label ) {
									printf(
										'<option value="%s" %s>%s</option>',
										esc_attr( $condition_key ),
										selected( in_array( $condition_key, $current_conditions, true ), true, false ),
										esc_html( $condition_label )
									);
								}
								?>
							</select>
							<span class="rx-hook-mini">
								<?php esc_html_e( 'Hold Ctrl/Cmd to select multiple conditions.', 'rx-theme' ); ?>
							</span>
						</p>

						<p>
							<label for="rx_roles"><?php esc_html_e( 'Allowed User Roles', 'rx-theme' ); ?></label>
							<select id="rx_roles" name="roles[]" multiple size="12">
								<?php
								$current_roles = $editing_item['roles'] ?? array();

								foreach ( $roles as $role_key => $role_data ) {
									printf(
										'<option value="%s" %s>%s</option>',
										esc_attr( $role_key ),
										selected( in_array( $role_key, $current_roles, true ), true, false ),
										esc_html( translate_user_role( $role_data['name'] ) )
									);
								}
								?>
							</select>
							<span class="rx-hook-mini">
								<?php esc_html_e( 'Leave empty to allow all roles.', 'rx-theme' ); ?>
							</span>
						</p>
					</div>

					<div class="rx-hook-grid">
						<p>
							<label for="rx_start_date"><?php esc_html_e( 'Start Date', 'rx-theme' ); ?></label>
							<input type="date" id="rx_start_date" name="start_date" value="<?php echo esc_attr( $editing_item['start_date'] ?? '' ); ?>">
							<span class="rx-hook-mini"><?php esc_html_e( 'Optional schedule start date.', 'rx-theme' ); ?></span>
						</p>

						<p>
							<label for="rx_end_date"><?php esc_html_e( 'End Date', 'rx-theme' ); ?></label>
							<input type="date" id="rx_end_date" name="end_date" value="<?php echo esc_attr( $editing_item['end_date'] ?? '' ); ?>">
							<span class="rx-hook-mini"><?php esc_html_e( 'Optional schedule end date.', 'rx-theme' ); ?></span>
						</p>
					</div>

					<div class="rx-hook-grid">
						<p>
							<label for="rx_wrapper_class"><?php esc_html_e( 'Wrapper CSS Class', 'rx-theme' ); ?></label>
							<input type="text" id="rx_wrapper_class" name="wrapper_class" value="<?php echo esc_attr( $editing_item['wrapper_class'] ?? '' ); ?>" placeholder="rx-header-ad custom-class">
						</p>

						<p>
							<label for="rx_wrapper_id"><?php esc_html_e( 'Wrapper HTML ID', 'rx-theme' ); ?></label>
							<input type="text" id="rx_wrapper_id" name="wrapper_id" value="<?php echo esc_attr( $editing_item['wrapper_id'] ?? '' ); ?>" placeholder="rx-special-banner">
						</p>
					</div>

					<p>
						<label>
							<input type="checkbox" name="disable_wrapper" value="1" <?php checked( ! empty( $editing_item['disable_wrapper'] ) ); ?>>
							<?php esc_html_e( 'Disable wrapper div', 'rx-theme' ); ?>
						</label>
					</p>

					<p class="submit rx-hook-actions">
						<button type="submit" class="button button-primary">
							<?php esc_html_e( 'Save Hook Block', 'rx-theme' ); ?>
						</button>

						<a href="<?php echo esc_url( admin_url( 'themes.php?page=' . self::PAGE_SLUG ) ); ?>" class="button">
							<?php esc_html_e( 'Add New / Reset Form', 'rx-theme' ); ?>
						</a>
					</p>
				</form>
			</div>

			<div class="rx-hook-card">
				<h2><?php esc_html_e( 'Saved Hook Blocks', 'rx-theme' ); ?></h2>

				<?php if ( empty( $items ) ) : ?>
					<p><?php esc_html_e( 'No hook blocks found.', 'rx-theme' ); ?></p>
				<?php else : ?>
					<table class="widefat striped">
						<thead>
							<tr>
								<th><?php esc_html_e( 'Title', 'rx-theme' ); ?></th>
								<th><?php esc_html_e( 'Hook', 'rx-theme' ); ?></th>
								<th><?php esc_html_e( 'Priority', 'rx-theme' ); ?></th>
								<th><?php esc_html_e( 'Type', 'rx-theme' ); ?></th>
								<th><?php esc_html_e( 'Status', 'rx-theme' ); ?></th>
								<th><?php esc_html_e( 'Actions', 'rx-theme' ); ?></th>
							</tr>
						</thead>

						<tbody>
							<?php foreach ( $items as $item ) : ?>
								<tr>
									<td>
										<strong><?php echo esc_html( $item['title'] ?? 'Untitled' ); ?></strong>
										<br>
										<span class="rx-hook-mini">
											ID: <?php echo esc_html( $item['id'] ?? '' ); ?>
										</span>
									</td>

									<td>
										<span class="rx-hook-badge">
											<?php echo esc_html( $item['hook'] ?? '' ); ?>
										</span>
									</td>

									<td><?php echo esc_html( $item['priority'] ?? 10 ); ?></td>

									<td><?php echo esc_html( $item['content_type'] ?? 'html' ); ?></td>

									<td>
										<?php if ( ! empty( $item['status'] ) && 'enabled' === $item['status'] ) : ?>
											<span class="rx-hook-badge"><?php esc_html_e( 'Enabled', 'rx-theme' ); ?></span>
										<?php else : ?>
											<span class="rx-hook-danger"><?php esc_html_e( 'Disabled', 'rx-theme' ); ?></span>
										<?php endif; ?>
									</td>

									<td>
										<div class="rx-hook-actions">
											<a class="button button-small" href="<?php echo esc_url( admin_url( 'themes.php?page=' . self::PAGE_SLUG . '&edit=' . rawurlencode( $item['id'] ) ) ); ?>">
												<?php esc_html_e( 'Edit', 'rx-theme' ); ?>
											</a>

											<form method="post" action="" style="display:inline;">
												<?php wp_nonce_field( self::NONCE_ACTION, self::NONCE_NAME ); ?>
												<input type="hidden" name="rx_hook_builder_action" value="duplicate_item">
												<input type="hidden" name="item_id" value="<?php echo esc_attr( $item['id'] ); ?>">
												<button type="submit" class="button button-small">
													<?php esc_html_e( 'Duplicate', 'rx-theme' ); ?>
												</button>
											</form>

											<form method="post" action="" style="display:inline;">
												<?php wp_nonce_field( self::NONCE_ACTION, self::NONCE_NAME ); ?>
												<input type="hidden" name="rx_hook_builder_action" value="toggle_item">
												<input type="hidden" name="item_id" value="<?php echo esc_attr( $item['id'] ); ?>">
												<button type="submit" class="button button-small">
													<?php
													echo ( ! empty( $item['status'] ) && 'enabled' === $item['status'] )
														? esc_html__( 'Disable', 'rx-theme' )
														: esc_html__( 'Enable', 'rx-theme' );
													?>
												</button>
											</form>

											<form method="post" action="" style="display:inline;">
												<?php wp_nonce_field( self::NONCE_ACTION, self::NONCE_NAME ); ?>
												<input type="hidden" name="rx_hook_builder_action" value="delete_item">
												<input type="hidden" name="item_id" value="<?php echo esc_attr( $item['id'] ); ?>">
												<button type="submit" class="button button-small rx-hook-confirm rx-hook-danger">
													<?php esc_html_e( 'Delete', 'rx-theme' ); ?>
												</button>
											</form>
										</div>
									</td>
								</tr>
							<?php endforeach; ?>
						</tbody>
					</table>
				<?php endif; ?>
			</div>

			<div class="rx-hook-card">
				<h2><?php esc_html_e( 'Import / Export', 'rx-theme' ); ?></h2>

				<div class="rx-hook-grid">
					<div>
						<h3><?php esc_html_e( 'Export JSON', 'rx-theme' ); ?></h3>
						<textarea id="rx-hook-export-json" readonly><?php echo esc_textarea( $export_json ); ?></textarea>
						<p>
							<button class="button rx-hook-copy" data-target="#rx-hook-export-json">
								<?php esc_html_e( 'Copy Export JSON', 'rx-theme' ); ?>
							</button>
						</p>
					</div>

					<div>
						<h3><?php esc_html_e( 'Import JSON', 'rx-theme' ); ?></h3>
						<form method="post" action="">
							<?php wp_nonce_field( self::NONCE_ACTION, self::NONCE_NAME ); ?>
							<input type="hidden" name="rx_hook_builder_action" value="import_items">
							<textarea name="import_json" placeholder="Paste exported JSON here"></textarea>

							<p>
								<label>
									<input type="checkbox" name="replace_existing" value="1">
									<?php esc_html_e( 'Replace existing hook blocks', 'rx-theme' ); ?>
								</label>
							</p>

							<p>
								<button type="submit" class="button button-primary rx-hook-confirm">
									<?php esc_html_e( 'Import Hook Blocks', 'rx-theme' ); ?>
								</button>
							</p>
						</form>
					</div>
				</div>
			</div>

			<div class="rx-hook-card">
				<h2><?php esc_html_e( 'How to Add RX Theme Hooks in Template Files', 'rx-theme' ); ?></h2>

				<p><?php esc_html_e( 'Use these examples inside your theme template files.', 'rx-theme' ); ?></p>

				<pre><code>&lt;?php do_action( 'rx_before_header' ); ?&gt;
&lt;?php do_action( 'rx_after_header' ); ?&gt;
&lt;?php do_action( 'rx_before_main' ); ?&gt;
&lt;?php do_action( 'rx_after_main' ); ?&gt;
&lt;?php do_action( 'rx_before_footer' ); ?&gt;
&lt;?php do_action( 'rx_after_footer' ); ?&gt;</code></pre>

				<p><?php esc_html_e( 'Shortcodes:', 'rx-theme' ); ?></p>

				<pre><code>[rx_hook_area hook="rx_before_header"]
[rx_hook_block id="your-block-id"]</code></pre>
			</div>
		</div>
		<?php
	}

	/**
	 * Admin notices.
	 *
	 * @return void
	 */
	private function admin_notices() {
		if ( empty( $_GET['rx_hook_notice'] ) ) {
			return;
		}

		$notice = sanitize_key( wp_unslash( $_GET['rx_hook_notice'] ) );

		$messages = array(
			'saved'      => __( 'Hook block saved successfully.', 'rx-theme' ),
			'deleted'    => __( 'Hook block deleted successfully.', 'rx-theme' ),
			'duplicated' => __( 'Hook block duplicated successfully.', 'rx-theme' ),
			'toggled'    => __( 'Hook block status changed successfully.', 'rx-theme' ),
			'imported'   => __( 'Hook blocks imported successfully.', 'rx-theme' ),
			'error'      => __( 'Something went wrong.', 'rx-theme' ),
		);

		$message = $messages[ $notice ] ?? $messages['error'];

		printf(
			'<div class="notice notice-success is-dismissible"><p>%s</p></div>',
			esc_html( $message )
		);
	}

	/**
	 * Handle admin actions.
	 *
	 * @return void
	 */
	public function handle_admin_actions() {
		if ( empty( $_POST['rx_hook_builder_action'] ) ) {
			return;
		}

		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		check_admin_referer( self::NONCE_ACTION, self::NONCE_NAME );

		$action = sanitize_key( wp_unslash( $_POST['rx_hook_builder_action'] ) );

		switch ( $action ) {
			case 'save_item':
				$this->save_item_from_post();
				break;

			case 'delete_item':
				$this->delete_item_from_post();
				break;

			case 'duplicate_item':
				$this->duplicate_item_from_post();
				break;

			case 'toggle_item':
				$this->toggle_item_from_post();
				break;

			case 'import_items':
				$this->import_items_from_post();
				break;
		}
	}

	/**
	 * Save item from POST.
	 *
	 * @return void
	 */
	private function save_item_from_post() {
		$items = $this->get_items();

		$id = ! empty( $_POST['item_id'] )
			? sanitize_text_field( wp_unslash( $_POST['item_id'] ) )
			: $this->generate_id();

		$item = array(
			'id'              => $id,
			'title'           => isset( $_POST['title'] ) ? sanitize_text_field( wp_unslash( $_POST['title'] ) ) : '',
			'status'          => isset( $_POST['status'] ) ? sanitize_key( wp_unslash( $_POST['status'] ) ) : 'enabled',
			'hook'            => isset( $_POST['hook'] ) ? sanitize_key( wp_unslash( $_POST['hook'] ) ) : 'wp_footer',
			'priority'        => isset( $_POST['priority'] ) ? absint( $_POST['priority'] ) : 10,
			'content_type'    => isset( $_POST['content_type'] ) ? sanitize_key( wp_unslash( $_POST['content_type'] ) ) : 'html',
			'content'         => isset( $_POST['content'] ) ? wp_kses_post( wp_unslash( $_POST['content'] ) ) : '',
			'condition_mode'  => isset( $_POST['condition_mode'] ) ? sanitize_key( wp_unslash( $_POST['condition_mode'] ) ) : 'all',
			'conditions'      => isset( $_POST['conditions'] ) ? $this->sanitize_array( wp_unslash( $_POST['conditions'] ) ) : array(),
			'logged_in'       => isset( $_POST['logged_in'] ) ? sanitize_key( wp_unslash( $_POST['logged_in'] ) ) : 'all',
			'device'          => isset( $_POST['device'] ) ? sanitize_key( wp_unslash( $_POST['device'] ) ) : 'all',
			'roles'           => isset( $_POST['roles'] ) ? $this->sanitize_array( wp_unslash( $_POST['roles'] ) ) : array(),
			'start_date'      => isset( $_POST['start_date'] ) ? sanitize_text_field( wp_unslash( $_POST['start_date'] ) ) : '',
			'end_date'        => isset( $_POST['end_date'] ) ? sanitize_text_field( wp_unslash( $_POST['end_date'] ) ) : '',
			'wrapper_class'   => isset( $_POST['wrapper_class'] ) ? sanitize_html_class( wp_unslash( $_POST['wrapper_class'] ) ) : '',
			'wrapper_id'      => isset( $_POST['wrapper_id'] ) ? sanitize_html_class( wp_unslash( $_POST['wrapper_id'] ) ) : '',
			'disable_wrapper' => ! empty( $_POST['disable_wrapper'] ) ? 1 : 0,
			'updated_at'      => current_time( 'mysql' ),
		);

		if ( empty( $item['title'] ) ) {
			$item['title'] = 'Untitled Hook Block';
		}

		$existing = $this->get_item( $id );

		if ( empty( $existing ) ) {
			$item['created_at'] = current_time( 'mysql' );
		} else {
			$item['created_at'] = $existing['created_at'] ?? current_time( 'mysql' );
		}

		$items[ $id ] = $item;

		$this->update_items( $items );
		$this->redirect_with_notice( 'saved' );
	}

	/**
	 * Delete item from POST.
	 *
	 * @return void
	 */
	private function delete_item_from_post() {
		$id    = isset( $_POST['item_id'] ) ? sanitize_text_field( wp_unslash( $_POST['item_id'] ) ) : '';
		$items = $this->get_items();

		if ( $id && isset( $items[ $id ] ) ) {
			unset( $items[ $id ] );
			$this->update_items( $items );
		}

		$this->redirect_with_notice( 'deleted' );
	}

	/**
	 * Duplicate item from POST.
	 *
	 * @return void
	 */
	private function duplicate_item_from_post() {
		$id    = isset( $_POST['item_id'] ) ? sanitize_text_field( wp_unslash( $_POST['item_id'] ) ) : '';
		$items = $this->get_items();

		if ( $id && isset( $items[ $id ] ) ) {
			$new_id = $this->generate_id();
			$new    = $items[ $id ];

			$new['id']         = $new_id;
			$new['title']      = ( $new['title'] ?? 'Hook Block' ) . ' Copy';
			$new['created_at'] = current_time( 'mysql' );
			$new['updated_at'] = current_time( 'mysql' );

			$items[ $new_id ] = $new;

			$this->update_items( $items );
		}

		$this->redirect_with_notice( 'duplicated' );
	}

	/**
	 * Toggle item status from POST.
	 *
	 * @return void
	 */
	private function toggle_item_from_post() {
		$id    = isset( $_POST['item_id'] ) ? sanitize_text_field( wp_unslash( $_POST['item_id'] ) ) : '';
		$items = $this->get_items();

		if ( $id && isset( $items[ $id ] ) ) {
			$current_status = $items[ $id ]['status'] ?? 'enabled';

			$items[ $id ]['status']     = 'enabled' === $current_status ? 'disabled' : 'enabled';
			$items[ $id ]['updated_at'] = current_time( 'mysql' );

			$this->update_items( $items );
		}

		$this->redirect_with_notice( 'toggled' );
	}

	/**
	 * Import items from POST.
	 *
	 * @return void
	 */
	private function import_items_from_post() {
		$json = isset( $_POST['import_json'] ) ? wp_unslash( $_POST['import_json'] ) : '';

		$decoded = json_decode( $json, true );

		if ( ! is_array( $decoded ) ) {
			$this->redirect_with_notice( 'error' );
		}

		$existing = ! empty( $_POST['replace_existing'] ) ? array() : $this->get_items();

		foreach ( $decoded as $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}

			$id = ! empty( $item['id'] ) ? sanitize_text_field( $item['id'] ) : $this->generate_id();

			$item['id']              = $id;
			$item['title']           = sanitize_text_field( $item['title'] ?? 'Imported Hook Block' );
			$item['status']          = sanitize_key( $item['status'] ?? 'disabled' );
			$item['hook']            = sanitize_key( $item['hook'] ?? 'wp_footer' );
			$item['priority']        = absint( $item['priority'] ?? 10 );
			$item['content_type']    = sanitize_key( $item['content_type'] ?? 'html' );
			$item['content']         = wp_kses_post( $item['content'] ?? '' );
			$item['condition_mode']  = sanitize_key( $item['condition_mode'] ?? 'all' );
			$item['conditions']      = isset( $item['conditions'] ) ? $this->sanitize_array( $item['conditions'] ) : array();
			$item['logged_in']       = sanitize_key( $item['logged_in'] ?? 'all' );
			$item['device']          = sanitize_key( $item['device'] ?? 'all' );
			$item['roles']           = isset( $item['roles'] ) ? $this->sanitize_array( $item['roles'] ) : array();
			$item['start_date']      = sanitize_text_field( $item['start_date'] ?? '' );
			$item['end_date']        = sanitize_text_field( $item['end_date'] ?? '' );
			$item['wrapper_class']   = sanitize_html_class( $item['wrapper_class'] ?? '' );
			$item['wrapper_id']      = sanitize_html_class( $item['wrapper_id'] ?? '' );
			$item['disable_wrapper'] = ! empty( $item['disable_wrapper'] ) ? 1 : 0;
			$item['updated_at']      = current_time( 'mysql' );
			$item['created_at']      = sanitize_text_field( $item['created_at'] ?? current_time( 'mysql' ) );

			$existing[ $id ] = $item;
		}

		$this->update_items( $existing );
		$this->redirect_with_notice( 'imported' );
	}

	/**
	 * Redirect with admin notice.
	 *
	 * @param string $notice Notice key.
	 * @return void
	 */
	private function redirect_with_notice( $notice ) {
		wp_safe_redirect(
			add_query_arg(
				array(
					'page'           => self::PAGE_SLUG,
					'rx_hook_notice' => sanitize_key( $notice ),
				),
				admin_url( 'themes.php' )
			)
		);
		exit;
	}

	/**
	 * Get saved items.
	 *
	 * @return array
	 */
	public function get_items() {
		$items = get_option( self::OPTION_NAME, array() );

		if ( ! is_array( $items ) ) {
			$items = array();
		}

		return $items;
	}

	/**
	 * Update saved items.
	 *
	 * @param array $items Items.
	 * @return bool
	 */
	public function update_items( $items ) {
		return update_option( self::OPTION_NAME, $items, false );
	}

	/**
	 * Get single item.
	 *
	 * @param string $id Item ID.
	 * @return array
	 */
	public function get_item( $id ) {
		$items = $this->get_items();

		return $items[ $id ] ?? array();
	}

	/**
	 * Generate item ID.
	 *
	 * @return string
	 */
	private function generate_id() {
		return 'rx_hook_' . wp_generate_password( 12, false, false );
	}

	/**
	 * Render one item.
	 *
	 * @param array $item Item.
	 * @param bool  $check_condition Whether to check condition.
	 * @return void
	 */
	public function render_item( $item, $check_condition = true ) {
		if ( $check_condition && ! $this->item_can_display( $item ) ) {
			return;
		}

		$content_type = $item['content_type'] ?? 'html';
		$content      = $item['content'] ?? '';

		if ( empty( $content ) ) {
			return;
		}

		$output = '';

		switch ( $content_type ) {
			case 'shortcode':
				$output = do_shortcode( $content );
				break;

			case 'template_part':
				$output = $this->get_template_part_output( $content, $item );
				break;

			case 'html':
			default:
				$output = do_shortcode( wp_kses_post( $content ) );
				break;
		}

		$output = apply_filters( 'rx_theme_hook_builder_output', $output, $item );

		if ( empty( $output ) ) {
			return;
		}

		if ( ! empty( $item['disable_wrapper'] ) ) {
			echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			return;
		}

		$classes = array(
			'rx-hook-builder-block',
			'rx-hook-builder-' . sanitize_html_class( $item['id'] ?? '' ),
		);

		if ( ! empty( $item['wrapper_class'] ) ) {
			$classes[] = sanitize_html_class( $item['wrapper_class'] );
		}

		$wrapper_id = ! empty( $item['wrapper_id'] ) ? sanitize_html_class( $item['wrapper_id'] ) : '';

		printf(
			'<div %s class="%s" data-rx-hook="%s">%s</div>',
			$wrapper_id ? 'id="' . esc_attr( $wrapper_id ) . '"' : '',
			esc_attr( implode( ' ', array_filter( $classes ) ) ),
			esc_attr( $item['hook'] ?? '' ),
			$output // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		);
	}

	/**
	 * Get template part output.
	 *
	 * @param string $path Template path.
	 * @param array  $item Item.
	 * @return string
	 */
	private function get_template_part_output( $path, $item ) {
		$path = trim( $path );
		$path = str_replace( array( '../', './', '.php' ), '', $path );
		$path = sanitize_text_field( $path );

		if ( empty( $path ) ) {
			return '';
		}

		ob_start();

		set_query_var( 'rx_hook_builder_item', $item );
		get_template_part( $path );

		return ob_get_clean();
	}

	/**
	 * Determine whether item can display.
	 *
	 * @param array $item Item.
	 * @return bool
	 */
	public function item_can_display( $item ) {
		if ( is_admin() ) {
			return false;
		}

		if ( empty( $item['status'] ) || 'enabled' !== $item['status'] ) {
			return false;
		}

		if ( ! $this->passes_date_rule( $item ) ) {
			return false;
		}

		if ( ! $this->passes_login_rule( $item ) ) {
			return false;
		}

		if ( ! $this->passes_role_rule( $item ) ) {
			return false;
		}

		if ( ! $this->passes_device_rule( $item ) ) {
			return false;
		}

		if ( ! $this->passes_condition_rule( $item ) ) {
			return false;
		}

		return apply_filters( 'rx_theme_hook_builder_can_display', true, $item );
	}

	/**
	 * Date rule.
	 *
	 * @param array $item Item.
	 * @return bool
	 */
	private function passes_date_rule( $item ) {
		$today      = current_time( 'Y-m-d' );
		$start_date = ! empty( $item['start_date'] ) ? $item['start_date'] : '';
		$end_date   = ! empty( $item['end_date'] ) ? $item['end_date'] : '';

		if ( $start_date && $today < $start_date ) {
			return false;
		}

		if ( $end_date && $today > $end_date ) {
			return false;
		}

		return true;
	}

	/**
	 * Login rule.
	 *
	 * @param array $item Item.
	 * @return bool
	 */
	private function passes_login_rule( $item ) {
		$logged_in = $item['logged_in'] ?? 'all';

		if ( 'logged_in' === $logged_in && ! is_user_logged_in() ) {
			return false;
		}

		if ( 'logged_out' === $logged_in && is_user_logged_in() ) {
			return false;
		}

		return true;
	}

	/**
	 * Role rule.
	 *
	 * @param array $item Item.
	 * @return bool
	 */
	private function passes_role_rule( $item ) {
		if ( empty( $item['roles'] ) || ! is_array( $item['roles'] ) ) {
			return true;
		}

		if ( ! is_user_logged_in() ) {
			return false;
		}

		$user = wp_get_current_user();

		if ( empty( $user->roles ) ) {
			return false;
		}

		return (bool) array_intersect( $item['roles'], $user->roles );
	}

	/**
	 * Device rule.
	 *
	 * @param array $item Item.
	 * @return bool
	 */
	private function passes_device_rule( $item ) {
		$device = $item['device'] ?? 'all';

		if ( 'all' === $device ) {
			return true;
		}

		$is_mobile = wp_is_mobile();

		if ( 'mobile' === $device && ! $is_mobile ) {
			return false;
		}

		if ( 'desktop' === $device && $is_mobile ) {
			return false;
		}

		return true;
	}

	/**
	 * Condition rule.
	 *
	 * @param array $item Item.
	 * @return bool
	 */
	private function passes_condition_rule( $item ) {
		$mode       = $item['condition_mode'] ?? 'all';
		$conditions = $item['conditions'] ?? array();

		if ( 'all' === $mode || empty( $conditions ) ) {
			return true;
		}

		$matched = false;

		foreach ( $conditions as $condition ) {
			if ( $this->condition_matches( $condition ) ) {
				$matched = true;
				break;
			}
		}

		if ( 'include' === $mode ) {
			return $matched;
		}

		if ( 'exclude' === $mode ) {
			return ! $matched;
		}

		return true;
	}

	/**
	 * Check individual condition.
	 *
	 * @param string $condition Condition.
	 * @return bool
	 */
	private function condition_matches( $condition ) {
		switch ( $condition ) {
			case 'front_page':
				return is_front_page();

			case 'home':
				return is_home();

			case 'singular':
				return is_singular();

			case 'single':
				return is_single();

			case 'page':
				return is_page();

			case 'attachment':
				return is_attachment();

			case 'archive':
				return is_archive();

			case 'category':
				return is_category();

			case 'tag':
				return is_tag();

			case 'author':
				return is_author();

			case 'date':
				return is_date();

			case 'search':
				return is_search();

			case '404':
				return is_404();

			case 'comments_open':
				return is_singular() && comments_open();

			case 'has_excerpt':
				return is_singular() && has_excerpt();

			case 'has_post_thumbnail':
				return is_singular() && has_post_thumbnail();

			case 'logged_in':
				return is_user_logged_in();

			case 'logged_out':
				return ! is_user_logged_in();

			default:
				return (bool) apply_filters( 'rx_theme_hook_builder_condition_matches', false, $condition );
		}
	}

	/**
	 * Get condition options.
	 *
	 * @return array
	 */
	private function get_condition_options() {
		return apply_filters(
			'rx_theme_hook_builder_condition_options',
			array(
				'front_page'         => 'Front Page',
				'home'               => 'Blog Home',
				'singular'           => 'Any Singular Post/Page',
				'single'             => 'Single Posts',
				'page'               => 'Pages',
				'attachment'         => 'Attachment Pages',
				'archive'            => 'Any Archive',
				'category'           => 'Category Archive',
				'tag'                => 'Tag Archive',
				'author'             => 'Author Archive',
				'date'               => 'Date Archive',
				'search'             => 'Search Results',
				'404'                => '404 Page',
				'comments_open'      => 'Comments Open',
				'has_excerpt'        => 'Posts/Pages With Excerpt',
				'has_post_thumbnail' => 'Posts/Pages With Featured Image',
				'logged_in'          => 'Logged-in Users',
				'logged_out'         => 'Logged-out Visitors',
			)
		);
	}

	/**
	 * Shortcode: render all items for a hook.
	 *
	 * Usage: [rx_hook_area hook="rx_before_header"]
	 *
	 * @param array $atts Shortcode attributes.
	 * @return string
	 */
	public function shortcode_hook_area( $atts ) {
		$atts = shortcode_atts(
			array(
				'hook' => '',
			),
			$atts,
			'rx_hook_area'
		);

		$hook = sanitize_key( $atts['hook'] );

		if ( empty( $hook ) ) {
			return '';
		}

		$items = $this->get_items();

		ob_start();

		foreach ( $items as $item ) {
			if ( ! empty( $item['hook'] ) && $hook === $item['hook'] ) {
				$this->render_item( $item );
			}
		}

		return ob_get_clean();
	}

	/**
	 * Shortcode: render one item by ID.
	 *
	 * Usage: [rx_hook_block id="rx_hook_xxxx"]
	 *
	 * @param array $atts Shortcode attributes.
	 * @return string
	 */
	public function shortcode_hook_block( $atts ) {
		$atts = shortcode_atts(
			array(
				'id' => '',
			),
			$atts,
			'rx_hook_block'
		);

		$id = sanitize_text_field( $atts['id'] );

		if ( empty( $id ) ) {
			return '';
		}

		$item = $this->get_item( $id );

		if ( empty( $item ) ) {
			return '';
		}

		ob_start();

		$this->render_item( $item );

		return ob_get_clean();
	}

	/**
	 * Sanitize simple array.
	 *
	 * @param mixed $array Array.
	 * @return array
	 */
	private function sanitize_array( $array ) {
		if ( ! is_array( $array ) ) {
			return array();
		}

		return array_values(
			array_filter(
				array_map(
					'sanitize_key',
					$array
				)
			)
		);
	}

	/**
	 * Print selected option.
	 *
	 * @param string $value Current value.
	 * @param string $label Label.
	 * @param string $current Current selected.
	 * @return void
	 */
	private function option( $value, $label, $current ) {
		printf(
			'<option value="%s" %s>%s</option>',
			esc_attr( $value ),
			selected( $current, $value, false ),
			esc_html( $label )
		);
	}
}

endif;

/**
 * Start RX Hook Builder.
 */
function rx_theme_hook_builder() {
	return RX_Theme_Hook_Builder::instance();
}

rx_theme_hook_builder();

/**
 * Helper function for theme templates.
 *
 * Usage:
 * rx_theme_do_hook( 'rx_before_header' );
 *
 * @param string $hook Hook name.
 * @return void
 */
function rx_theme_do_hook( $hook ) {
	do_action( sanitize_key( $hook ) );
}

2. Load it from functions.php

Add this in your functions.php:

require_once get_template_directory() . '/inc/builders/hook-builder.php';

3. Add theme hook positions in your template files

Example in header.php:

<?php do_action( 'rx_before_site' ); ?>
<?php do_action( 'rx_before_header' ); ?>

<header id="masthead" class="site-header">
	<?php do_action( 'rx_header' ); ?>
</header>

<?php do_action( 'rx_after_header' ); ?>

Example in footer.php:

<?php do_action( 'rx_before_footer' ); ?>

<footer id="colophon" class="site-footer">
	<?php do_action( 'rx_footer' ); ?>
</footer>

<?php do_action( 'rx_after_footer' ); ?>
<?php do_action( 'rx_after_site' ); ?>

Example in single.php:

<?php do_action( 'rx_before_single' ); ?>

<main id="primary" class="site-main">
	<?php do_action( 'rx_before_content' ); ?>

	<?php
	while ( have_posts() ) :
		the_post();

		do_action( 'rx_inside_article_top' );

		the_content();

		do_action( 'rx_inside_article_bottom' );

	endwhile;
	?>

	<?php do_action( 'rx_after_content' ); ?>
</main>

<?php do_action( 'rx_after_single' ); ?>

4. How to use in WordPress admin

After adding the file:

Go to:

Appearance → RX Hook Builder

Then create a hook block like:

<div class="rx-top-banner">
	<p>Welcome to RX Theme</p>
</div>

Choose hook:

rx_before_header

Or choose WordPress hook:

wp_footer

You can also use shortcode content:

Error: Contact form not found.

Or template part:

template-parts/hooks/header-ad

That will load:

template-parts/hooks/header-ad.php

Important safety note

I did not add raw PHP eval() execution inside the admin content box. That would be dangerous for a live WordPress theme. Instead, this version supports safer advanced methods:

do_action()
shortcode
template_part
filter extension

This is the better professional way.

Leave a Reply

Your email address will not be published. Required fields are marked *