Cookbook

Cookbook: Extend Form Submit Lifecycle

1 min read

Use Forms hooks when you need custom validation, extra payload fields, or side effects around public submissions.

Hook reference: Hooks: Forms. User overview: Notifal Forms Overview.

Register callbacks after WordPress loads plugins. Prefer constants from FilterHooks and ActionHooks.

Example: block a specific form

<?php
/**
 * Plugin Name: My Notifal Forms Guard
 * Description: Example block for notifal/forms/submit/allow/{form_id}.
 * Version: 1.0.0
 * Requires PHP: 7.4
 */

defined( 'ABSPATH' ) || exit;

use Notifal\Infrastructure\WordPress\Hooks\FilterHooks;

/**
 * Block form CPT 42 unless a honeypot-style marketing consent field is checked.
 *
 * @return void
 */
add_action( 'plugins_loaded', static function () {
	$form_id = 42;

	add_filter(
		sprintf( FilterHooks::FORMS_SUBMIT_ALLOW_FORM, $form_id ),
		static function ( $allowed, $schema, $fields ) {
			$consent = isset( $fields['marketing_consent'] ) ? (string) $fields['marketing_consent'] : '';

			if ( $consent === '1' || $consent === 'on' || $consent === 'yes' ) {
				return $allowed;
			}

			return array(
				'message' => __( 'Please accept marketing consent to continue.', 'my-notifal-forms-guard' ),
				'code'    => 'consent_required',
				'field'   => 'marketing_consent',
			);
		},
		10,
		3
	);
} );

Example: custom field validation

<?php
defined( 'ABSPATH' ) || exit;

use Notifal\Infrastructure\WordPress\Hooks\FilterHooks;

add_action( 'plugins_loaded', static function () {
	add_filter(
		sprintf( FilterHooks::FORMS_VALIDATE_FIELD_FORM, 42, 'email' ),
		static function ( $errors, $field, $value ) {
			$email = is_string( $value ) ? $value : '';

			if ( $email !== '' && false !== strpos( $email, '@blocked.test' ) ) {
				$errors[] = 'blocked_domain';
			}

			return $errors;
		},
		10,
		3
	);
} );

Example: mutate fields before validation

<?php
defined( 'ABSPATH' ) || exit;

use Notifal\Infrastructure\WordPress\Hooks\FilterHooks;

add_filter(
	FilterHooks::FORMS_SUBMIT_FIELDS,
	static function ( $fields, $schema ) {
		$form_id = isset( $schema['form_id'] ) ? absint( $schema['form_id'] ) : 0;

		if ( 42 === $form_id ) {
			$fields['referral'] = 'campaign-a';
		}

		return $fields;
	},
	10,
	2
);

Example: react after a successful store

<?php
defined( 'ABSPATH' ) || exit;

use Notifal\Infrastructure\WordPress\Hooks\ActionHooks;

add_action(
	sprintf( ActionHooks::FORMS_SUBMIT_AFTER_FORM, 42 ),
	static function ( $submission_id, $schema, $fields ) {
		// Side effects only: logging, CRM push, cache busting.
		do_action( 'my_crm_push_lead', (int) $submission_id, $fields );
	},
	10,
	3
);

Example: frontend JavaScript

document.addEventListener( 'notifal:form:before-submit:42', function ( event ) {
	event.detail.fields.referral = 'campaign-a';
} );

document.addEventListener( 'notifal:form:validate:42', function ( event ) {
	if ( ! event.detail.fields.email ) {
		event.detail.valid = false;
		event.preventDefault();
	}
} );

document.addEventListener( 'notifal:form:success:42', function ( event ) {
	// Optional analytics after AJAX success.
	window.dataLayer = window.dataLayer || [];
	window.dataLayer.push( {
		event: 'notifal_form_success',
		formId: 42,
		message: event.detail.message || '',
	} );
} );

Cancelable events: validate and before-submit. Call event.preventDefault() or set detail.valid = false on validate to stop the flow.

Safety checklist

  • Sanitize any values you inject into $fields or emails.
  • Do not put secrets into notifal/forms/submit_response.
  • Prefer form-scoped hooks when logic applies to one Form CPT only.
  • Keep frontend listeners lightweight; Forms assets load only when a form is present.