Skip to main content
AbstractFlow implements FlowInterface and provides all the boilerplate so you can focus on your flow’s unique logic. Extend it and implement the three abstract methods.
namespace UTB\ProductBuilder\Flows;

abstract class AbstractFlow implements FlowInterface
{
    // ...
}

Extending AbstractFlow

use UTB\ProductBuilder\Flows\AbstractFlow;

class MyFlow extends AbstractFlow
{
    public function get_id(): string        { return 'my_flow'; }
    public function get_name(): string      { return 'My Flow'; }
    public function get_description(): string { return 'Does something custom.'; }
    public function get_icon(): string      { return 'dashicons-star-filled'; }

    public function get_shortcodes(): array { return ['my_flow_form' => 'render_custom_content']; }
    public function get_ajax_endpoints(): array { return []; }

    // The three required methods:
    protected function render_custom_content(): void { /* output HTML */ }
    public function prepare_cart_metadata(array $post_data): array { return []; }
    public function validate_cart_data(array $post_data) { return true; }
}

Methods you MUST override

render_custom_content(): void

Outputs the HTML for the product page. Called by render_flow_wrapper(), which wraps the output in <div class="utb-product-flow-container id-{flow_id}">.
protected abstract function render_custom_content(): void;

prepare_cart_metadata(array $post_data): array

Builds the metadata array saved to the WooCommerce cart item and order. See FlowInterface::prepare_cart_metadata() for full documentation.
public abstract function prepare_cart_metadata(array $post_data): array;

validate_cart_data(array $post_data)

Validates the add-to-cart form submission. Return true on success or an error string on failure. See FlowInterface::validate_cart_data() for full documentation.
public abstract function validate_cart_data(array $post_data);

Methods you CAN override

enqueue_assets(): void

Called on wp_enqueue_scripts. The base implementation checks is_product() and applies_to_product() and returns early if neither applies. Override to enqueue flow-specific JavaScript and CSS.
public function enqueue_assets(): void
{
    // Base class exits early if not on a matching product page.
    // Override to call wp_enqueue_script() / wp_enqueue_style().
}

get_shortcodes(): array

Returns shortcodes to register. Defaults to an empty array in the base class (the interface requires it; AbstractFlow does not provide a default). Override to register shortcodes.

get_ajax_endpoints(): array

Returns AJAX endpoints to register. Both wp_ajax_{action} and wp_ajax_nopriv_{action} are registered for each entry, so all endpoints are accessible to logged-in and guest users. Override to add endpoints.

Hooks registered by init()

Calling AbstractFlow::init() (which happens automatically via FlowRegistry) registers these WordPress/WooCommerce hooks:
HookMethodPriorityArgs
wpmaybe_setup_landing50
woocommerce_add_to_cart_validationvalidate_add_to_cart103
woocommerce_add_cart_item_dataadd_cart_item_data_filter103
woocommerce_before_calculate_totalsapply_dynamic_pricing201
woocommerce_checkout_create_order_line_itemsave_order_item_meta104
wp_enqueue_scriptsenqueue_assets
Shortcodes are registered with add_shortcode() and AJAX endpoints with add_action() during init() based on what your overridden get_shortcodes() and get_ajax_endpoints() return.

Product landing cleanup

When applies_to_product() returns true for the current product, setup_product_landing() removes the following WooCommerce elements to make room for the flow’s custom UI: Removed from woocommerce_before_main_content:
  • woocommerce_breadcrumb (priority 20)
Removed from woocommerce_sidebar:
  • woocommerce_get_sidebar (priority 10)
Removed from woocommerce_single_product_summary:
  • woocommerce_template_single_title (priority 5)
  • woocommerce_template_single_rating (priority 10)
  • woocommerce_template_single_price (priority 10)
  • woocommerce_template_single_excerpt (priority 20)
  • woocommerce_template_single_add_to_cart (priority 30)
  • woocommerce_template_single_meta (priority 40)
  • woocommerce_template_single_sharing (priority 50)
Removed from woocommerce_after_single_product_summary:
  • woocommerce_output_product_data_tabs (priority 10)
  • woocommerce_upsell_display (priority 15)
  • woocommerce_output_related_products (priority 20)
After removing standard elements, the method injects the flow wrapper into woocommerce_single_product_summary at priority 30 and applies inline CSS to hide any price markup that the theme may still render.

Utility methods

Nonce helpers

protected function create_nonce(string $action): string
Creates a WordPress nonce scoped to this flow. The nonce action is "{flow_id}_{action}".
protected function verify_nonce(string $nonce, string $action): bool
Verifies a nonce created by create_nonce(). Returns true if valid.

AJAX response helpers

protected function ajax_success($data = []): void
Sends a JSON success response via wp_send_json_success() and terminates.
protected function ajax_error(string $message, $data = [], int $status_code = 400): void
Sends a JSON error response via wp_send_json_error() with the given HTTP status code and terminates. The $data parameter is merged into the response body alongside the message key.

Sanitization helpers

protected function sanitize_text($value): string
Runs wp_unslash() then sanitize_text_field() on $value.
protected function sanitize_email_field($value): string
Runs wp_unslash() then sanitize_email() on $value.
protected function sanitize_int($value): int
Casts $value to int.

Example: minimal custom flow

use UTB\ProductBuilder\Flows\AbstractFlow;

class WorkshopFlow extends AbstractFlow
{
    public function get_id(): string        { return 'workshop_flow'; }
    public function get_name(): string      { return 'Workshop Enrollment'; }
    public function get_description(): string { return 'Enrolls students in workshops.'; }
    public function get_icon(): string      { return 'dashicons-groups'; }

    public function get_shortcodes(): array
    {
        return ['workshop_form' => 'render_custom_content'];
    }

    public function get_ajax_endpoints(): array
    {
        return ['workshop_get_slots' => 'handle_get_slots'];
    }

    protected function render_custom_content(): void
    {
        echo '<form id="workshop-form" method="post">';
        wp_nonce_field($this->create_nonce('add_cart'));
        echo '<button type="submit">Enroll</button></form>';
    }

    public function validate_cart_data(array $post_data)
    {
        if (!$this->verify_nonce($post_data['_wpnonce'] ?? '', 'add_cart')) {
            return 'Security check failed.';
        }
        return true;
    }

    public function prepare_cart_metadata(array $post_data): array
    {
        return [
            '_utb_form_data' => json_encode(['source' => 'workshop_form']),
        ];
    }

    public function handle_get_slots(): void
    {
        $this->ajax_success(['slots' => 12]);
    }
}

Build docs developers (and LLMs) love