Source: mt-fields-api.php

<?php
/**
 * Fields API.
 *
 * @category API
 * @package  My Tickets
 * @author   Joe Dolson
 * @license  GPLv2 or later
 * @link     https://www.joedolson.com/my-tickets/
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
} // Exit if accessed directly

add_action( 'init', 'mt_add_actions' );
/**
 * Parent function to insert custom filters.
 */
function mt_add_actions() {
	// adds field to Add to Cart form.
	add_filter( 'mt_add_to_cart_fields', 'mt_custom_field', 10, 2 );
	// Save field data to cookie/user meta for use in cart.
	add_action( 'mt_add_to_cart_ajax_field_handler', 'mt_handle_custom_field', 10, 2 );
	// Display field data in shopping cart.
	add_filter( 'mt_show_in_cart_fields', 'mt_show_custom_field', 10, 6 );
	// Insert submitted data into Payment post meta.
	add_action( 'mt_save_payment_fields', 'mt_insert_custom_field', 10, 3 );
	// Display field data in tickets list.
	add_filter( 'mt_custom_tickets_fields', 'mt_custom_tickets_fields', 10, 4 );
}

/**
 * Get My Tickets custom fields.
 *
 * @param string $context Where is this field being used. Input, cart, ticket, receipt.
 *
 * @return array
 */
function mt_get_custom_fields( $context ) {
	$fields = array();
	/**
	 * Hook to add custom fields.
	 *
	 * @hook mt_custom_fields
	 *
	 * @param {array}  $fields Array of custom fields. Default empty.
	 * @param {string} $context Where is this function being called. Input, cart, ticket, receipt, etc.
	 *
	 * @return {array}
	 */
	$fields = apply_filters( 'mt_custom_fields', $fields, $context );

	return $fields;
}

/**
 * Add custom fields to tickets.
 *
 * @param string $output Field output.
 * @param int    $event_id Event ID.
 * @param int    $payment_id Payment ID.
 * @param string $sep Field separator.
 *
 * @return string
 */
function mt_custom_tickets_fields( $output, $event_id, $payment_id, $sep ) {
	$custom_fields = mt_get_custom_fields( 'tickets' );
	$return        = '';
	$last_value    = '';
	foreach ( $custom_fields as $name => $field ) {
		$data   = get_post_meta( $payment_id, $name );
		$return = apply_filters( 'mt_custom_ticket_display_field', '', $data, $name, $payment_id );
		if ( ! empty( $data ) && '' === $return ) {

			foreach ( $data as $d ) {
				if ( ! isset( $field['display_callback'] ) ) {
					$display_value = stripslashes( $d[ $name ] );
				} else {
					$display_value = call_user_func( $field['display_callback'], $d[ $name ], 'payment', $field );
				}

				if ( '' !== $display_value && (string) $d['event_id'] === (string) $event_id ) {
					$added_value = $field['title'] . ' - ' . $display_value;
					if ( $added_value === $last_value ) {
						continue;
					} else {
						$add_value  = apply_filters( 'mt_custom_ticket_format_output', $added_value . $sep, $payment_id, $name );
						$output    .= $add_value;
						$last_value = $added_value;
					}
				}
			}
		}
	}
	$return = ( $return ) ? '<div class="mt-custom-fields">' . $return . '</div>' : '';

	return $output . $return;
}

/**
 * Check whether defined custom ticket fields should be shown on a given event.
 *
 * @param array $field Field info.
 * @param int   $event_id Event ID.
 *
 * @return bool
 */
function mt_apply_custom_field( $field, $event_id ) {
	$return = false;
	if ( ! isset( $field['context'] ) || 'global' === $field['context'] ) {
		$return = true;
	}
	if ( is_numeric( $field['context'] ) && (int) $field['context'] === (int) $event_id ) {
		$return = true;
	}
	if ( is_array( $field['context'] ) && in_array( $event_id, $field['context'], true ) ) {
		$return = true;
	}

	/**
	 * Filter the boolean value that indicates whether a custom field should be shown on an event.
	 *
	 * @hook mt_apply_custom_field_rules
	 *
	 * @param {bool}  $return True to return this field.
	 * @param {array} $field Array of custom field characteristics.
	 * @param {int}   $event_id Event being displayed.
	 *
	 * @return {bool}
	 */
	return apply_filters( 'mt_apply_custom_field_rules', $return, $field, $event_id );
}

/**
 * Process array of custom field definitions to return output HTML.
 *
 * @param array $fields Field data.
 * @param int   $event_id Event ID.
 *
 * @return string
 */
function mt_custom_field( $fields, $event_id ) {
	$custom_fields = mt_get_custom_fields( 'input' );
	$output        = '';
	foreach ( $custom_fields as $name => $field ) {
		$continue = mt_apply_custom_field( $field, $event_id );
		/**
		 * Modify a custom field's characteristics prior to rendering.
		 *
		 * @hook mt_field_parameters
		 *
		 * @param {array} $field Array of field information.
		 * @param {int}   $event_id The event being rendered.
		 *
		 * @return {array}
		 */
		$field = apply_filters( 'mt_field_parameters', $field, $event_id );
		if ( $continue && is_array( $field ) ) {
			$user_value = esc_attr( stripslashes( mt_get_data( $name . '_' . $event_id ) ) );
			$required   = isset( $field['required'] ) ? ' required' : '';
			$req_label  = isset( $field['required'] ) ? ' <span class="required">' . __( 'Required', 'my-tickets' ) . '</span>' : '';
			switch ( $field['input_type'] ) {
				case 'text':
				case 'number':
				case 'email':
				case 'url':
				case 'date':
				case 'tel':
					$output = "<input type='" . $field['input_type'] . "' name='$name' id='$name' value='$user_value'$required />";
					break;
				case 'textarea':
					$output = "<textarea rows='6' cols='60' name='$name' id='$name'$required>$user_value</textarea>";
					break;
				case 'select':
					if ( isset( $field['input_values'] ) ) {
						$output = "<select name='$name' id='$name'$required>";
						foreach ( $field['input_values'] as $value ) {
							$value = esc_attr( stripslashes( $value ) );
							if ( $value === $user_value ) {
								$selected = " selected='selected'";
							} else {
								$selected = '';
							}
							$output .= "<option value='" . esc_attr( stripslashes( $value ) ) . "'$selected>" . $value . "</option>\n";
						}
						$output .= '</select>';
					}
					break;
				case 'checkbox':
				case 'radio':
					if ( isset( $field['input_values'] ) ) {
						$value = $field['input_values'];
						if ( '' !== $user_value ) {
							$checked = ' checked="checked"';
						} else {
							$checked = '';
						}
						$output = "<input type='" . $field['input_type'] . "' name='$name' id='$name' value='" . esc_attr( stripslashes( $value ) ) . "'$checked $required />";
					}
					break;
				default:
					$output = "<input type='text' name='$name' id='$name' value='$user_value' $required />";
			}
			$class   = 'mt-' . sanitize_html_class( $field['title'] );
			$fields .= "<div class='mt-ticket-field $class'><label for='$name'>" . $field['title'] . $req_label . '</label> ' . $output . '</div>';
		}
	}

	return $fields;
}

/**
 * Call user functions to sanitize and save custom data with current user's shopping cart.
 *
 * @param mixed $saved data to save.
 * @param array $submit Submitted data.
 *
 * @return mixed
 */
function mt_handle_custom_field( $saved, $submit ) {
	$custom_fields = mt_get_custom_fields( 'sanitize' );
	foreach ( $custom_fields as $name => $field ) {
		if ( isset( $submit[ $name ] ) ) {
			if ( ! isset( $field['sanitize_callback'] ) || ( isset( $field['sanitize_callback'] ) && ! function_exists( $field['sanitize_callback'] ) ) ) {
				// if no sanitization is provided, we'll prep it for SQL and strip tags.
				if ( is_array( $submit[ $name ] ) ) {
					$sanitized = map_deep( $submit[ $name ], 'sanitize_text_field' );
				} else {
					$sanitized = sanitize_text_field( $submit[ $name ] );
				}
			} else {
				$sanitized = call_user_func( $field['sanitize_callback'], urldecode( $submit[ $name ] ) );
			}
			$event_id = $submit['mt_event_id'];
			mt_save_data( $sanitized, $name . '_' . $event_id );
		}
	}

	return $saved;
}

/**
 * Display saved data in shopping cart and add hidden input field to pass into payment creation.
 *
 * @param string   $content Shopping cart html.
 * @param int      $event_id Event ID.
 * @param int|bool $payment Payment ID if available.
 * @param string   $type Ticket type for this line item.
 * @param int      $count Number of tickets of this type in cart. May change dynamically.
 * @param string   $format Whether we're in the cart or confirmation view.
 *
 * @return string
 */
function mt_show_custom_field( $content, $event_id, $payment, $type, $count, $format ) {
	$custom_fields = mt_get_custom_fields( 'display' );
	$return        = '';
	foreach ( $custom_fields as $name => $field ) {
		$data = mt_get_data( $name . '_' . $event_id );
		if ( ! isset( $field['display_callback'] ) || ( isset( $field['display_callback'] ) && ! function_exists( $field['display_callback'] ) ) ) {
			$display_value = sanitize_text_field( $data );
		} else {
			$display_value = call_user_func( $field['display_callback'], $data, 'cart', $field );
		}
		$return .= $display_value . "<input type='hidden' name='{$name}[$event_id]' value='" . esc_attr( $data ) . "' />";
	}
	$return = ( $return ) ? '<div class="mt-custom-fields">' . $return . '</div>' : '';

	return $content . $return;
}

/**
 * Insert post meta data into payment field.
 *
 * @param integer $payment_id Payment ID.
 * @param array   $post POST data.
 * @param array   $purchased Purchase info.
 */
function mt_insert_custom_field( $payment_id, $post, $purchased ) {
	$custom_fields = mt_get_custom_fields( 'save' );
	foreach ( $custom_fields as $name => $field ) {
		if ( isset( $post[ $name ] ) ) {
			foreach ( $post[ $name ] as $event_id => $data ) {
				if ( '' !== $data ) {
					if ( ! isset( $field['sanitize_callback'] ) || ( isset( $field['sanitize_callback'] ) && ! function_exists( $field['sanitize_callback'] ) ) ) {
						// if no sanitization is provided, we'll prep it for SQL and strip tags.
						$sanitized = sanitize_text_field( $data );
					} else {
						$sanitized = call_user_func( $field['sanitize_callback'], $data );
					}
					$data = array(
						'event_id' => $event_id,
						$name      => $sanitized,
					);
					add_post_meta( $payment_id, $name, $data );
					/**
					 * Execute a custom action when saving custom field data.
					 *
					 * @hook mt_save_custom_field
					 *
					 * @param {int}    $payment_id Payment ID for this purchase.
					 * @param {int}    $event_id Event tickets purchased for.
					 * @param {string} $name Custom field key being saved.
					 * @param {array}  $sanitized Data submitted by user post-sanitization.
					 * @param {array}  $purchased Full purchase data.
					 */
					do_action( 'mt_custom_field_saved', $payment_id, $event_id, $name, $data, $purchased );
				}
			}
		}
	}
}

/**
 * Display custom field in payment post manager.
 *
 * @param string $content Form HTML.
 * @param int    $payment_id Payment ID.
 *
 * @return string
 */
function mt_show_payment_field( $content, $payment_id ) {
	$custom_fields = mt_get_custom_fields( 'admin' );
	$output        = '';
	foreach ( $custom_fields as $name => $field ) {
		$data = get_post_meta( $payment_id, $name );
		/**
		 * Customize the output of custom fields in the admin Payment record.
		 *
		 * @hook mt_custom_display_field
		 *
		 * @param {string} $output_html. Default empty string.
		 * @param {mixed}  $data Saved data from post meta.
		 * @param {string} $name Field name array key.
		 *
		 * @return {string}
		 */
		$return = apply_filters( 'mt_custom_display_field', '', $data, $name );
		if ( '' === $return ) {
			foreach ( $data as $d ) {
				if ( ! isset( $field['display_callback'] ) ) {
					$display_value = stripslashes( $d[ $name ] );
				} else {
					$display_value = call_user_func( $field['display_callback'], $d[ $name ], 'payment', $field );
				}
				if ( '' !== $display_value ) {
					$event_title = get_the_title( $d['event_id'] );
					$output     .= apply_filters( 'mt_custom_data_format_output', "<p><strong>$event_title</strong>:<br />$field[title] - $display_value</p>", $payment_id, $name );
				}
			}
		} else {
			$output .= $return;
		}
	}

	return $content . $output;
}

/**
 * Display custom field on tickets/receipts
 *
 * @param int            $payment_id Payment ID.
 * @param string|boolean $custom_field Custom field data to display.
 *
 * @return string
 */
function mt_show_custom_data( $payment_id, $custom_field = false ) {
	$custom_fields = mt_get_custom_fields( 'receipt' );
	$output        = '';
	foreach ( $custom_fields as $name => $field ) {
		if ( false === $custom_field || $custom_field === $name ) {
			$data = get_post_meta( $payment_id, $name );
			/**
			 * Customize the display of a custom field.
			 *
			 * @hook mt_custom_display_field
			 *
			 * @param {string} $return Return value of the field.
			 * @param {mixed}  $data Value stored in post meta.
			 * @param {string} $name Field name.
			 *
			 * @return {string}
			 */
			$return = apply_filters( 'mt_custom_display_field', '', $data, $name );
			if ( '' === $return ) {
				foreach ( $data as $d ) {
					if ( ! isset( $field['display_callback'] ) ) {
						$display_value = stripslashes( $d[ $name ] );
					} else {
						$display_value = call_user_func( $field['display_callback'], $d[ $name ], 'payment', $field );
					}
					if ( '' !== $display_value ) {
						$event_title = get_the_title( $d['event_id'] );
						$output     .= apply_filters( 'mt_custom_data_format_output', "<p><strong>$event_title</strong>:<br />$field[title] - $display_value</p>", $payment_id, $custom_field );
					}
				}
			} else {
				$output .= $return;
			}
		}
	}

	return $output;
}