WooCommerce Code Reference

AbstractFieldType.php

Source code

<?php
declare( strict_types = 1);

namespace Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldTypes;

use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsSchema\Validation;
use WP_Error;

/**
 * Base class for additional checkout field types.
 *
 * Provides passthrough defaults so each field type only overrides the behavior it needs. New
 * type-level behavior should be added here with a default implementation so existing subclasses
 * keep working.
 *
 * phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter -- The passthrough defaults keep the
 * signature subclasses override.
 */
abstract class AbstractFieldType {

	/**
	 * Adds type-specific defaults to a field's validation schema.
	 *
	 * @since 11.2.0
	 *
	 * @param array $schema The supplied validation schema.
	 * @return array The schema with defaults applied.
	 */
	public function prepare_validation_schema( array $schema ): array {
		return $schema;
	}

	/**
	 * Validates the options that apply to every field type: callbacks, hidden state, and rule schemas.
	 *
	 * Subclasses adding checks should call this parent method first.
	 *
	 * @param array $options The options supplied during field registration.
	 * @return bool False if an error should prevent registration, true otherwise.
	 */
	public function validate_options( array $options ): bool {
		$id = $options['id'];

		if ( empty( $options['label'] ) ) {
			return $this->registration_error( $id, 'The field label is required.', '8.6.0' );
		}

		foreach ( array( 'sanitize_callback', 'validate_callback' ) as $callback ) {
			if ( ! empty( $options[ $callback ] ) && ! is_callable( $options[ $callback ] ) ) {
				return $this->registration_error( $id, sprintf( 'The %s must be a valid callback.', $callback ), '8.6.0' );
			}
		}

		if ( ! empty( $options['hidden'] ) && true === $options['hidden'] ) {
			// Not an error: the field is still registered, just as a visible one.
			$this->doing_it_wrong( sprintf( 'Registering a field with hidden set to true is not supported. The field "%s" will be registered as visible.', $id ), '8.6.0' );
		}

		foreach ( array( 'required', 'hidden', 'validation' ) as $rule_field ) {
			if ( empty( $options[ $rule_field ] ) || ( 'validation' !== $rule_field && is_bool( $options[ $rule_field ] ) ) ) {
				continue;
			}

			$valid = Validation::is_valid_schema( $options[ $rule_field ] );

			if ( is_wp_error( $valid ) ) {
				return $this->registration_error( $id, $rule_field . ': ' . $valid->get_error_message(), '8.6.0' );
			}
		}

		return true;
	}

	/**
	 * Processes and validates the options supplied during field registration.
	 *
	 * @param array $field_data The field data array to be updated.
	 * @param array $options    The options supplied during field registration.
	 * @return array|false The updated $field_data array, or false if an error should prevent registration.
	 */
	final public function process_options( array $field_data, array $options ) {
		$field_data['attributes'] = $this->process_attributes( $field_data['id'], $field_data['attributes'] );

		return $this->process_type_options( $field_data, $options );
	}

	/**
	 * Processes the type-specific options supplied during field registration.
	 *
	 * @param array $field_data The field data array, with the options common to every type already applied.
	 * @param array $options    The options supplied during field registration.
	 * @return array|false The updated $field_data array, or false if an error should prevent registration.
	 */
	protected function process_type_options( array $field_data, array $options ) {
		return $field_data;
	}

	/**
	 * Processes the attributes supplied during field registration.
	 *
	 * Invalid attributes are dropped with a warning rather than preventing registration.
	 *
	 * @param string $id         The field ID.
	 * @param mixed  $attributes The attributes supplied during field registration.
	 * @return array The processed attributes.
	 */
	protected function process_attributes( $id, $attributes ): array {
		if ( empty( $attributes ) ) {
			return [];
		}

		if ( ! is_array( $attributes ) ) {
			$this->doing_it_wrong( sprintf( 'An invalid attributes value was supplied when registering field with id: "%s". Attributes must be a non-empty array.', $id ), '8.6.0' );
			return [];
		}

		// These are formatted in camelCase because React components expect them that way.
		$allowed_attributes = [ 'maxLength', 'readOnly', 'pattern', 'autocomplete', 'autocapitalize', 'title' ];

		$valid_attributes = array_filter(
			$attributes,
			function ( $_, $key ) use ( $allowed_attributes ) {
				return in_array( $key, $allowed_attributes, true ) || strpos( $key, 'aria-' ) === 0 || strpos( $key, 'data-' ) === 0;
			},
			ARRAY_FILTER_USE_BOTH
		);

		if ( count( $attributes ) !== count( $valid_attributes ) ) {
			$invalid_attributes = array_keys( array_diff_key( $attributes, $valid_attributes ) );
			$this->doing_it_wrong( sprintf( 'Invalid attribute found when registering field with id: "%s". Attributes: %s are not allowed.', $id, implode( ', ', $invalid_attributes ) ), '8.6.0' );
		}

		return array_map( 'esc_attr', $valid_attributes );
	}

	/**
	 * Sanitizes a submitted value for this field type.
	 *
	 * @param mixed $value The submitted value.
	 * @param array $field The field.
	 * @return mixed The sanitized value.
	 */
	public function sanitize( $value, array $field ) {
		return $value;
	}

	/**
	 * The validate_callback a field gets when it does not declare its own: rejects empty required fields.
	 *
	 * Unlike validate(), a field's validate_callback can be replaced at registration, so nothing here is
	 * mandatory for the type.
	 *
	 * @param mixed $value The submitted value.
	 * @param array $field The field.
	 * @return WP_Error|void An error if the value is invalid.
	 */
	public function default_validate( $value, $field ) {
		if ( true === $field['required'] && empty( $value ) ) {
			return new WP_Error(
				'woocommerce_required_checkout_field',
				sprintf(
					// translators: %s is field key.
					__( 'The field %s is required.', 'woocommerce' ),
					$field['id']
				)
			);
		}
	}

	/**
	 * Validates a submitted value against the constraints of this field type.
	 *
	 * @param mixed $value The submitted value.
	 * @param array $field The field.
	 * @return \WP_Error|null Error if the value is not valid for the field type, null otherwise.
	 */
	public function validate( $value, array $field ) {
		return null;
	}

	/**
	 * Formats a stored value for display based on the field type.
	 *
	 * @param mixed $value The stored value.
	 * @param array $field The field.
	 * @return mixed The formatted value.
	 */
	public function format_value( $value, array $field ) {
		return $value;
	}

	/**
	 * Converts a stored meta value into the type the rest of checkout works with.
	 *
	 * @since 11.2.0
	 *
	 * @param mixed $value The stored value.
	 * @return mixed The converted value.
	 */
	public function from_storage( $value ) {
		return $value;
	}

	/**
	 * Applies type-specific keywords to a field's REST API value schema.
	 *
	 * @param array $field_schema The schema built for the field so far.
	 * @param array $field        The field.
	 * @return array The updated schema.
	 */
	public function prepare_value_schema( array $field_schema, array $field ): array {
		return $field_schema;
	}

	/**
	 * Applies type-specific arguments to a field before it is rendered with woocommerce_form_field().
	 *
	 * @param array $form_field The woocommerce_form_field() arguments built from the field.
	 * @return array The updated arguments.
	 */
	public function prepare_form_field( array $form_field ): array {
		return $form_field;
	}

	/**
	 * Reports a field registration error and prevents the field from being registered.
	 *
	 * @param string $id      The ID of the field being registered.
	 * @param string $reason  The reason the field cannot be registered.
	 * @param string $version The version the misuse was introduced in.
	 * @return false
	 */
	protected function registration_error( string $id, string $reason, string $version ): bool {
		return $this->doing_it_wrong( sprintf( 'Unable to register field with id: "%s". %s', $id, $reason ), $version );
	}

	/**
	 * Reports a field registration misuse that does not prevent the field from being registered.
	 *
	 * @param string $message The message describing the misuse.
	 * @param string $version The version the misuse was introduced in.
	 * @return false
	 */
	protected function doing_it_wrong( string $message, string $version ): bool {
		_doing_it_wrong( 'woocommerce_register_additional_checkout_field', esc_html( $message ), esc_html( $version ) );
		return false;
	}
}