Skip to main content
A Flow is the unit of behavior in UTB Product Builder. Each flow owns a WooCommerce product type’s full lifecycle: what the product page looks like, how cart data is collected and validated, how the price is calculated, and what order metadata is persisted.

The FlowInterface contract

Every flow must implement UTB\ProductBuilder\Flows\FlowInterface. The interface enforces the following methods:
interface FlowInterface {
    // Identity
    public function get_id(): string;          // Unique slug, e.g. 'utb_cep_programs'
    public function get_name(): string;        // Human-readable label for admin UI
    public function get_description(): string; // Description for admin UI
    public function get_icon(): string;        // Dashicon name, e.g. 'dashicons-awards'

    // Lifecycle
    public function init(): void;
    public function applies_to_product(int $product_id): bool;

    // WordPress integration
    public function get_shortcodes(): array;      // ['tag' => 'callback_method']
    public function get_ajax_endpoints(): array;  // ['action' => 'callback_method']

    // Cart pipeline
    public function validate_cart_data(array $post_data);          // true | error string
    public function prepare_cart_metadata(array $post_data): array;
    public function calculate_dynamic_price(array $cart_item_data): ?float;
}
validate_cart_data returns true on success or a localized error string on failure. The AbstractFlow wrapper catches this value and calls wc_add_notice() automatically, so concrete flows never interact with WooCommerce notices directly.

FlowRegistry

FlowRegistry is a static service that holds all registered flows and their product assignments in memory.

Registration

Flows are registered during Plugin::register_flows(), which runs on the plugins_loaded hook:
private function register_flows(): void {
    FlowRegistry::register(new CertificadosFlow());
    FlowRegistry::register(new CEPFlow());

    // Extension point for third-party flows
    do_action('utb_pb_register_flows');
}
FlowRegistry::register(FlowInterface $flow) stores the flow instance keyed by $flow->get_id().

Loading product assignments

After registration, FlowRegistry::load_assignments() reads product→flow mappings from wp_utb_form_configs:
public static function load_assignments(): void {
    global $wpdb;
    $table_name = $wpdb->prefix . 'utb_form_configs';

    $results = $wpdb->get_results(
        "SELECT product_id, flow_id FROM {$table_name}",
        ARRAY_A
    );
    // Stored in self::$product_assignments[$product_id] => $flow_id
}
Assignments are loaded once per request and cached in the static property FlowRegistry::$product_assignments.

Routing a product to its flow

$flow = FlowRegistry::get_flow_for_product($product_id);
// Returns FlowInterface|null
This is the core routing call. If no assignment exists for the product, the method returns null and the plugin leaves the WooCommerce product page untouched.

Initialization

FlowRegistry::init_all() iterates every registered flow and calls $flow->init(), which in turn registers shortcodes, AJAX endpoints, and WooCommerce hooks.

AbstractFlow lifecycle

AbstractFlow implements the common hook wiring so concrete flows only need to provide business logic.

Hook registration (init)

When AbstractFlow::init() is called it registers:
  • Shortcodes — from get_shortcodes(). Each entry in the returned array maps a shortcode tag to a method name on the flow.
  • AJAX endpoints — from get_ajax_endpoints(). Each action is registered for both wp_ajax_{action} and wp_ajax_nopriv_{action}.
  • WooCommerce hooks — hard-wired in AbstractFlow::init():
add_action('wp', [$this, 'maybe_setup_landing'], 50);
add_filter('woocommerce_add_to_cart_validation', [$this, 'validate_add_to_cart'], 10, 3);
add_filter('woocommerce_add_cart_item_data', [$this, 'add_cart_item_data_filter'], 10, 3);
add_action('woocommerce_before_calculate_totals', [$this, 'apply_dynamic_pricing'], 20, 1);
add_action('woocommerce_checkout_create_order_line_item', [$this, 'save_order_item_meta'], 10, 4);
add_action('wp_enqueue_scripts', [$this, 'enqueue_assets']);

Product page takeover

AbstractFlow::maybe_setup_landing() fires on wp. If the current page is a product and the flow owns it, setup_product_landing() removes all default WooCommerce output and injects the flow’s own HTML:
1

Remove standard WooCommerce elements

Strips breadcrumbs, title, price, excerpt, meta, sharing, add-to-cart button, product tabs, upsells, and related products from the WooCommerce action queue.
2

Inject flow wrapper

Adds render_flow_wrapper() to woocommerce_single_product_summary at priority 30. This outputs <div class="utb-product-flow-container"> and calls the abstract render_custom_content() method that each concrete flow implements.
3

Suppress duplicate content

Hooks suppress_detail_content onto the_content and remove_product_tabs onto woocommerce_product_tabs to prevent theme content duplication.
4

Inject cleanup CSS

Adds aggressive inline CSS targeting price, cart form, and tab elements to guarantee they are hidden regardless of the active theme.

Cart pipeline

HookAbstractFlow methodWhat it does
woocommerce_add_to_cart_validationvalidate_add_to_cartCalls validate_cart_data($_POST); converts errors to WC notices
woocommerce_add_cart_item_dataadd_cart_item_data_filterCalls prepare_cart_metadata($_POST); appends _utb_flow_id and a unique key
woocommerce_before_calculate_totalsapply_dynamic_pricingIterates cart items owned by this flow and sets price via calculate_dynamic_price()
woocommerce_checkout_create_order_line_itemsave_order_item_metaPersists all _utb_* prefixed keys from cart item data to order line item meta

Extension point: utb_pb_register_flows

Third-party plugins and themes can register custom flows by hooking into utb_pb_register_flows:
add_action('utb_pb_register_flows', function() {
    UTB\ProductBuilder\Flows\FlowRegistry::register(
        new MyPlugin\Flows\PostgraduateFlow()
    );
});
The custom flow must implement FlowInterface. Extending AbstractFlow is recommended so WooCommerce hook wiring is inherited automatically.
The utb_pb_register_flows action fires during plugins_loaded at priority 10. Your plugin must be loaded before or at the same priority for the action to fire. Use add_action('plugins_loaded', ..., 9) or earlier if needed.

Built-in flows

CEPFlow

Flow ID: utb_cep_programsHandles continuing education program enrollments. Renders a student data form with a program selector populated from wp_utb_cep_programs. Integrates with the Banner API via ApiConnectionManager('cep_integrator') to validate student identity and apply role-based discounts.Shortcode: [utb_cep_form]AJAX actions: cep_calculate_discount, utb_cep_price

CertificadosFlow

Flow ID: certificados_academicosHandles academic certificate requests with pricing that varies by certificate type, delivery format (digital/physical), and academic level (undergraduate/graduate). Reads certificate catalog and price matrix from wp_utb_certificates and wp_utb_certificate_prices.Shortcodes: [utb_cert_form], [utb_cert_catalog_modal]AJAX actions: utb_get_certs, utb_cert_price

Build docs developers (and LLMs) love