Cookbook

Cookbook: Add Custom Dynamic Tag

2 min read

Register custom dynamic tags so editors can insert placeholders like {store_phone} into Notifal templates (HTML Builder, Block Editor, Elementor widgets).

Notifal fires notifal/tag/register at the end of RegisterTags::register() and passes the shared TagManager instance. Your callback calls $manager->registerTag( new Tag( ... ) ).

Hook reference: Hooks: Tags and Dynamic Data.

Registration timing

TagManager is created lazily the first time Notifal resolves it from the container. The notifal/tag/register action runs during that first resolution, which typically happens during WordPress init priority 0.

Register your callback early, directly in your plugin bootstrap file or on plugins_loaded. Do not wait for notifal/initialized, because that action fires after modules boot and the tag registry may already be built.

Tag class overview

new Tag(
    string $key,        // Placeholder key without braces, e.g. "store_phone"
    string $label,      // Admin UI label
    Closure $resolver,  // function ( array $context, string $tagKey ): string
    string $category,   // TagCategory constant or custom category slug
    string $description // Optional help text in tag picker
);

Resolver $context may include domain objects depending on the content source, for example:

  • $context['user'] , user DTO when user context exists
  • $context['product'] , product DTO for product pools
  • $context['order'] , order DTO for order pools
  • $context['post'], $context['page'] , WordPress post objects on singular views
  • $context['is_preview'] , boolean, true in admin preview mode

Use preview fallbacks when is_preview is true so the tag picker shows sample output.

Example: static site-wide tag

<?php
/**
 * Plugin Name: My Notifal Custom Tags
 * Description: Registers custom dynamic tags for Notifal templates.
 * Version: 1.0.0
 * Requires PHP: 7.4
 */

defined( 'ABSPATH' ) || exit;

use Notifal\Domain\Tags\Tag;
use Notifal\Domain\Tags\TagManager;
use Notifal\Domain\Tags\Enums\TagCategory;
use Notifal\Infrastructure\WordPress\Hooks\ActionHooks;

/**
 * Register custom tags when Notifal builds the tag registry.
 *
 * @param TagManager $manager Shared TagManager instance from Notifal core.
 * @return void
 */
add_action( ActionHooks::TAG_REGISTER, 'my_notifal_register_custom_tags' );

/**
 * Register one or more custom tags on the TagManager.
 *
 * @param TagManager $manager TagManager passed by Notifal during registration.
 * @return void
 */
function my_notifal_register_custom_tags( TagManager $manager ): void {
    // Register a simple tag that reads a site option.
    $manager->registerTag(
        new Tag(
            'store_phone',
            __( 'Store Phone Number', 'my-notifal-tags' ),
            function ( array $context ): string {
                // Read the option value from the database.
                $phone = get_option( 'my_store_phone', '' );

                // Return sanitized text safe for HTML output contexts.
                return sanitize_text_field( (string) $phone );
            },
            TagCategory::GENERAL,
            __( 'Displays the store phone number from site settings.', 'my-notifal-tags' )
        )
    );
}

Use {store_phone} in any Notifal template.

Example: dynamic meta-style tag

Notifal core uses patterns like {user_meta_first_name}. You can register a parameterized key with {key} in the tag definition:

<?php

defined( 'ABSPATH' ) || exit;

use Notifal\Domain\Tags\Tag;
use Notifal\Domain\Tags\TagManager;
use Notifal\Domain\Tags\Enums\TagCategory;
use Notifal\Infrastructure\WordPress\Hooks\ActionHooks;

add_action( ActionHooks::TAG_REGISTER, function ( TagManager $manager ): void {
    $manager->registerTag(
        new Tag(
            'acme_option_{key}',
            __( 'ACME Plugin Option', 'my-notifal-tags' ),
            function ( array $context, string $tagKey ): string {
                // Extract the suffix after acme_option_ from the matched tag key.
                preg_match( '/acme_option_(.+)/', $tagKey, $matches );
                $option_name = isset( $matches[1] ) ? sanitize_key( $matches[1] ) : '';

                // Bail when the option name could not be parsed.
                if ( $option_name === '' ) {
                    return '';
                }

                // Read and sanitize the option value.
                $value = get_option( $option_name, '' );

                // Provide a preview placeholder in the admin tag preview UI.
                if ( $value === '' && ! empty( $context['is_preview'] ) ) {
                    return esc_html__( 'Sample Option Value', 'my-notifal-tags' );
                }

                return sanitize_text_field( (string) $value );
            },
            TagCategory::GENERAL,
            __( 'Reads a WordPress option: {acme_option_my_setting}', 'my-notifal-tags' )
        )
    );
} );

Template usage: {acme_option_my_setting}.

Example: WooCommerce-aware product tag

<?php

defined( 'ABSPATH' ) || exit;

use Notifal\Domain\Tags\Tag;
use Notifal\Domain\Tags\TagManager;
use Notifal\Domain\Tags\Enums\TagCategory;
use Notifal\Infrastructure\WordPress\Hooks\ActionHooks;

add_action( ActionHooks::TAG_REGISTER, function ( TagManager $manager ): void {
    $manager->registerTag(
        new Tag(
            'product_brand',
            __( 'Product Brand', 'my-notifal-tags' ),
            function ( array $context ): string {
                // Require a product object in the render context.
                if ( empty( $context['product'] ) || ! is_object( $context['product'] ) ) {
                    return '';
                }

                // Call getMeta when the product DTO exposes taxonomy or meta data.
                if ( ! method_exists( $context['product'], 'getMeta' ) ) {
                    return '';
                }

                // Read brand meta (adjust meta key to match your store setup).
                $brand = $context['product']->getMeta( 'brand' );

                // Preview fallback for empty values in the template editor.
                if ( ( $brand === '' || $brand === null ) && ! empty( $context['is_preview'] ) ) {
                    return esc_html__( 'Sample Brand', 'my-notifal-tags' );
                }

                return sanitize_text_field( (string) $brand );
            },
            TagCategory::PRODUCTS,
            __( 'Displays the product brand meta field.', 'my-notifal-tags' )
        )
    );
} );

Categories and settings UI

Built-in categories live in TagCategory (products, orders, users, posts, pages, general, cart, etc.).

Add custom categories with the notifal/tag/categories filter if you need a dedicated group in Global Settings tag toggles.

Duplicate tag keys throw InvalidTagException. Prefix keys with your plugin slug, for example acme_store_phone.

After registration, Notifal fires notifal/tag/manager/on_registered with the Tag instance if you need cross-plugin bookkeeping.

HookPurpose
notifal/tag/registerRegister tags on TagManager (this cookbook)
notifal/tag/categoriesAdd tag category slugs
notifal/tag/manager/on_registeredFires after each successful registerTag()
notifal/register_comment_tagsPro comment tag extension point

REST: tags are also exposed under notifal/v1 (see REST API Reference).

Cookbook series

  1. Cookbook: Modify Notification Before Display
  2. Cookbook: Add Custom Dynamic Tag (you are here)
  3. Cookbook: Customize Template Import
  4. Cookbook: Extend Display Rules
  5. Cookbook: Register a Custom HTML Builder Widget

Hook articles