diff --git a/form-actions/readme.md b/form-actions/readme.md deleted file mode 100644 index 8187d3e..0000000 --- a/form-actions/readme.md +++ /dev/null @@ -1,147 +0,0 @@ -# Form Actions API - -Creating a Breakdance Form Action involves two steps - -1. Create an Action class to represent your field -2. Register the action class with Breakdance - -# Creating an action class - -To get started create a new PHP class. Your class will need to extend the Breakdance base Action class `Breakdance\Forms\Actions\Action` - -## Required Methods - -There are three mandatory methods that must be implemented in your action class - -**name** - -The name method takes no arguments and returns a string that will be used to identify your action in the Form Builder Actions dropdown menu - -```php -/** - * @return string - */ -public function name() { - return 'My Action'; -} -``` - -**slug** - -The slug method takes no arguments and returns a string to identify the form action. This should be unique across all form actions, so it is recommended to prefix the slug appropriately. - -```php -/** - * @return string -*/ -public function slug() -{ - return 'my_plugin_form_action'; -} -``` - -**run** - -The run method accepts three arguments, `$form, $settings, $extra` and is called when the form has been submitted - -```php -/** -* Log the form submission to a file -* -* @param array $form -* @param array $settings -* @param array $extra -* @return array success or error message -*/ -public function run($form, $settings, $extra) -{ - try { - $this->writeToFile($extra['formId'], $extra['fields']); - } catch(Exception $e) { - return ['type' => 'error', 'message' => $e->getMessage()]; - } - - return ['type' => 'success', 'message' => 'Submission logged to file']; -} - -``` - -### Run Arguments - -$**form** - -The form argument contains all the form fields, their builder settings and the selected values - -- type: the field type -- name: the field name -- options: an array of available options for checkbox, radio or select inputs -- value: the submitted value of the field -- originalValue: the default/original value of the field - -**$settings** - -The settings argument contains an array of the configured form settings from the Breakdance builder - -**$extra** - -The extra argument contains additional data - -- files: An array of uploaded files -- fields: the submitted form fields in an `$id ⇒ $value` style array -- formId: The ID of the form -- postId: The ID of the post the form was submitted from -- ip: the submitters IP address -- referer: The form submitters referrer URL -- userAgent: The form submitters user agent string -- userId: The form submitters user ID (if applicable) - -### Responses - -The response should be an array that contains a `type` and `message` key. - -- type: either `error` or `success` -- message: a string message that will be displayed to admins with the submission - -**Success** - -```php -public function run($form, $settings, $extra) -{ - ... - return ['type' => 'success', 'message' => 'Submission logged to file']; -} - -``` - -**Error** - -```php -public function run($form, $settings, $extra) -{ - ... - return ['type' => 'error', 'message' => 'Could not write to file']; -} -``` - -## Register The Action - -Register the action by calling the registerAction helper and passing an instance of your action class - -**Note:** To prevent file loading race conditions, it is recommended to call the register helper from inside a WordPress action, e.g init. - -```php - -// register-actions.php included by your plugin - -add_action('init', function() { - // fail if Breakdance is not installed and available - if (!function_exists('\Breakdance\Forms\Actions\registerAction') || !class_exists('\Breakdance\Forms\Actions\Action')) { - return; - } - - require_once('my-action.php'); - - \Breakdance\Forms\Actions\registerAction(new MyAction()); - -}); -``` diff --git a/forms/creating-custom-actions.md b/forms/creating-custom-actions.md new file mode 100644 index 0000000..de1fd7b --- /dev/null +++ b/forms/creating-custom-actions.md @@ -0,0 +1,288 @@ +# Form Actions API + +Creating a Breakdance Form Action involves two steps + +1. Create an Action class to represent your field +2. Register the action class with Breakdance + +# Creating an action class + +To get started create a new PHP class. Your class will need to extend the Breakdance base Action class `Breakdance\Forms\Actions\Action` + +## Required Methods + +There are three mandatory methods that must be implemented in your action class + +**name** + +The name method takes no arguments and returns a string that will be used to identify your action in the Form Builder Actions dropdown menu + +```php +/** + * @return string + */ +public function name() { + return 'My Action'; +} +``` + +**slug** + +The slug method takes no arguments and returns a string to identify the form action. This should be unique across all form actions, so it is recommended to prefix the slug appropriately. + +```php +/** + * @return string +*/ +public function slug() +{ + return 'my_plugin_form_action'; +} +``` + +**run** + +The run method accepts three arguments, `$form, $settings, $extra` and is called when the form has been submitted + +```php +/** +* Log the form submission to a file +* +* @param array $form +* @param array $settings +* @param array $extra +* @return array success or error message +*/ +public function run($form, $settings, $extra) +{ + try { + $this->writeToFile($extra['formId'], $extra['fields']); + } catch(Exception $e) { + return ['type' => 'error', 'message' => $e->getMessage()]; + } + + return ['type' => 'success', 'message' => 'Submission logged to file']; +} + +``` + +### Run Arguments + +$**form** + +The form argument contains all the form fields, their builder settings and the selected values + +- type: the field type +- name: the field name +- options: an array of available options for checkbox, radio or select inputs +- value: the submitted value of the field +- originalValue: the default/original value of the field + +**$settings** + +The settings argument contains an array of the configured form settings from the Breakdance builder + +**$extra** + +The extra argument contains additional data + +- files: An array of uploaded files +- fields: the submitted form fields in an `$id ⇒ $value` style array +- formId: The ID of the form +- postId: The ID of the post the form was submitted from +- ip: the submitters IP address +- referer: The form submitters referrer URL +- userAgent: The form submitters user agent string +- userId: The form submitters user ID (if applicable) + +### Responses + +The response should be an array that contains a `type` and `message` key. + +- type: either `error` or `success` +- message: a string message that will be displayed to admins with the submission + +**Success** + +```php +public function run($form, $settings, $extra) +{ + ... + return ['type' => 'success', 'message' => 'Submission logged to file']; +} + +``` + +**Error** + +```php +public function run($form, $settings, $extra) +{ + ... + return ['type' => 'error', 'message' => 'Could not write to file']; +} +``` + +## Register The Action + +Register the action by calling the registerAction helper and passing an instance of your action class + +**Note:** To prevent file loading race conditions, it is recommended to call the register helper from inside a WordPress action, e.g init. + +```php + +// register-actions.php included by your plugin + +add_action('init', function() { + // fail if Breakdance is not installed and available + if (!function_exists('\Breakdance\Forms\Actions\registerAction') || !class_exists('\Breakdance\Forms\Actions\Action')) { + return; + } + + require_once('my-action.php'); + + \Breakdance\Forms\Actions\registerAction(new MyAction()); +}); +``` + +## Full Example + +Here's a complete example of a custom form action that automatically publishes recipe submissions to your blog: + +```php + 'recipes']); + $category_id = is_wp_error($result) ? 1 : $result['term_id']; + } else { + $category_id = $category->term_id; + } + + // Build the post content with recipe details + $content = ''; + + // Add description if available + if (!empty($fields['description'])) { + $content .= '

' . wp_kses_post($fields['description']) . '

'; + } + + // Add ingredients section + if (!empty($fields['ingredients'])) { + $content .= '

Ingredients

'; + $content .= '
'; + $content .= wpautop(wp_kses_post($fields['ingredients'])); + $content .= '
'; + } + + // Add instructions section + if (!empty($fields['instructions'])) { + $content .= '

Instructions

'; + $content .= '
'; + $content .= wpautop(wp_kses_post($fields['instructions'])); + $content .= '
'; + } + + // Add cooking time if available + if (!empty($fields['cooking_time'])) { + $content .= '

Cooking Time: ' . esc_html($fields['cooking_time']) . '

'; + } + + // Add submitter name to content + if (!empty($fields['your_name'])) { + $content .= '

Recipe submitted by: ' . esc_html($fields['your_name']) . '

'; + } + + // Create the post + $post_id = wp_insert_post([ + 'post_type' => 'post', + 'post_title' => sanitize_text_field($fields['recipe_name']), + 'post_content' => $content, + 'post_status' => 'publish', + 'post_category' => [$category_id], + ]); + + // Check for errors + if (is_wp_error($post_id)) { + return [ + 'type' => 'error', + 'message' => 'Failed to publish recipe: ' . $post_id->get_error_message() + ]; + } + + return [ + 'type' => 'success', + 'message' => 'Thank you! Your recipe has been published.' + ]; + } +} +``` + +**Registration file (register-recipe-action.php):** + +```php + 'text|email|tel|textarea|file|etc', + 'value' => 'sanitized user submitted value', + 'originalValue' => 'original submitted value (may be array)', + 'label' => 'Field Label', + 'advanced' => [ + 'id' => 'field_id', + 'required' => true|false, + 'conditional' => true|false, + // ... other advanced settings + ], + // ... other field properties based on field type +] +``` + +## Usage Examples + +### Basic Validation + +```php +add_filter('breakdance_form_validate_field', function($fieldErrors, $field, $formId, $postId) { + // Validate phone number format + if ($field['type'] === 'tel' && !empty($field['value'])) { + if (!preg_match('/^\d{10}$/', $field['value'])) { + $fieldErrors->add( + 'invalid_phone', + 'Please enter a valid 10-digit phone number.' + ); + } + } + + return $fieldErrors; +}, 10, 4); +``` + +### Validate Specific Field by ID + +```php +add_filter('breakdance_form_validate_field', function($fieldErrors, $field, $formId, $postId) { + $fieldId = $field['advanced']['id'] ?? ''; + + // Validate a specific field + if ($fieldId === 'custom_zip_code' && !empty($field['value'])) { + if (!preg_match('/^\d{5}(-\d{4})?$/', $field['value'])) { + $fieldErrors->add( + 'invalid_zip', + 'Please enter a valid ZIP code (12345 or 12345-6789).' + ); + } + } + + return $fieldErrors; +}, 10, 4); +``` + +### Validate Based on Form ID + +```php +add_filter('breakdance_form_validate_field', function($fieldErrors, $field, $formId, $postId) { + // Only validate for specific forms + if ($formId === 123) { + $fieldId = $field['advanced']['id'] ?? ''; + + if ($fieldId === 'age' && !empty($field['value'])) { + $age = intval($field['value']); + if ($age < 18) { + $fieldErrors->add( + 'age_restriction', + 'You must be 18 or older to submit this form.' + ); + } + } + } + + return $fieldErrors; +}, 10, 4); +``` + +### Advanced Validation with Multiple Conditions + +```php +add_filter('breakdance_form_validate_field', function($fieldErrors, $field, $formId, $postId) { + $fieldId = $field['advanced']['id'] ?? ''; + + // Custom email domain validation + if ($field['type'] === 'email' && !empty($field['value'])) { + $allowedDomains = ['company.com', 'example.org']; + $email = $field['value']; + $domain = substr(strrchr($email, "@"), 1); + + if (!in_array($domain, $allowedDomains)) { + $fieldErrors->add( + 'invalid_domain', + sprintf( + 'Email must be from one of these domains: %s', + implode(', ', $allowedDomains) + ) + ); + } + } + + // Password strength validation + if ($fieldId === 'password' && !empty($field['value'])) { + $password = $field['value']; + + if (strlen($password) < 8) { + $fieldErrors->add('password_length', 'Password must be at least 8 characters long.'); + } + + if (!preg_match('/[A-Z]/', $password)) { + $fieldErrors->add('password_uppercase', 'Password must contain at least one uppercase letter.'); + } + + if (!preg_match('/[0-9]/', $password)) { + $fieldErrors->add('password_number', 'Password must contain at least one number.'); + } + } + + return $fieldErrors; +}, 10, 4); +``` + +### API Validation + +```php +add_filter('breakdance_form_validate_field', function($fieldErrors, $field, $formId, $postId) { + $fieldId = $field['advanced']['id'] ?? ''; + + // Validate coupon code against external API + if ($fieldId === 'coupon_code' && !empty($field['value'])) { + $couponCode = $field['value']; + + // Call external API to validate coupon + $response = wp_remote_get("https://api.example.com/validate-coupon?code={$couponCode}"); + + if (is_wp_error($response)) { + $fieldErrors->add('coupon_error', 'Unable to validate coupon code. Please try again.'); + } else { + $body = json_decode(wp_remote_retrieve_body($response), true); + + if (!$body['valid']) { + $fieldErrors->add('invalid_coupon', 'This coupon code is not valid.'); + } + } + } + + return $fieldErrors; +}, 10, 4); +``` + +### Database Validation + +```php +add_filter('breakdance_form_validate_field', function($fieldErrors, $field, $formId, $postId) { + $fieldId = $field['advanced']['id'] ?? ''; + + // Check if username already exists + if ($fieldId === 'username' && !empty($field['value'])) { + $username = sanitize_user($field['value']); + + if (username_exists($username)) { + $fieldErrors->add('username_exists', 'This username is already taken.'); + } + } + + // Check if email already exists + if ($field['type'] === 'email' && $fieldId === 'user_email' && !empty($field['value'])) { + if (email_exists($field['value'])) { + $fieldErrors->add('email_exists', 'An account with this email already exists.'); + } + } + + return $fieldErrors; +}, 10, 4); +``` + +## Best Practices + +### 1. Always Check Field Type or ID + +Only validate fields that are relevant to your custom validation logic: + +```php +if ($field['type'] === 'email' && !empty($field['value'])) { + // Your validation logic +} +``` + +### 2. Check for Empty Values + +Don't validate empty values unless you need to enforce a custom required field rule: + +```php +if (!empty($field['value'])) { + // Your validation logic +} +``` + +### 3. Return the Error Object + +Always return the `$fieldErrors` object, even if you didn't add any errors: + +```php +return $fieldErrors; +``` + +### 4. Use Descriptive Error Codes and Messages + +Make error codes unique and messages user-friendly: + +```php +$fieldErrors->add('invalid_phone_format', 'Phone number must be in format: (123) 456-7890'); +``` + +### 5. Consider Performance + +Avoid heavy operations (like API calls) unless necessary. Consider caching results: + +```php +$cacheKey = 'validated_' . md5($field['value']); +$cached = get_transient($cacheKey); + +if ($cached !== false) { + if (!$cached) { + $fieldErrors->add('validation_error', 'Invalid value.'); + } +} else { + // Perform expensive validation + $isValid = expensive_validation($field['value']); + set_transient($cacheKey, $isValid, HOUR_IN_SECONDS); + + if (!$isValid) { + $fieldErrors->add('validation_error', 'Invalid value.'); + } +} +``` + +### 6. Sanitize User Input + +Always sanitize field values before using them in comparisons or database queries: + +```php +$fieldValue = sanitize_text_field($field['value']); +``` + +### 7. Use Priority Wisely + +Set appropriate priority if your validation depends on other validations: + +```php +// Run after other validations +add_filter('breakdance_form_validate_field', 'my_validation', 20, 4); + +// Run before other validations +add_filter('breakdance_form_validate_field', 'my_validation', 5, 4); +``` + +## Hook Execution Flow + +1. Form is submitted +2. For each field in the form: + - A fresh `WP_Error` object is created + - The `breakdance_form_validate_field` filter is called + - All hooked functions receive the same error object + - Functions can add errors using `$fieldErrors->add()` + - If the returned error object has errors, they are merged into the main validation bag +3. If the main bag has errors, form submission fails and errors are displayed +4. If no errors, form submission continues + +## Error Display + +Errors added through this hook will be displayed to users along with other form validation errors. The exact display depends on the form's error message settings. + +## Related Hooks + +- `breakdance_form_run_action_{action_slug}` - Control whether a specific form action should run +- `breakdance_form_honeypot_triggered` - Triggered when honeypot spam protection is triggered + +## Changelog + +- **v1.0** - Hook introduced + +## Support + +For more information about Breakdance forms and available hooks, visit the [Breakdance Documentation](https://breakdance.com/documentation/). +