label, for select fields. * - column (bool) Show as a grid column (default true). * - default (bool) Visible by default (default true). * - sortable (string) Orderby key understood by the entity, or empty. * - width (int) Column width in px. * - get (callable) function( $object ): string. * - set (callable) function( $object, $value ): void|WP_Error. * - editable (callable) function( $object ): bool. Optional. * - display (callable) function( $object ): string. Text shown when locked. Optional. */ class BDBE_Fields { /** * Types that can never be edited. * * @var string[] */ const READONLY_TYPES = array( 'readonly', 'image' ); /** * Sanitise and validate one raw value for a field. * * @param array $def Field definition. * @param mixed $value Raw value from the request. * @return string|WP_Error Clean value, or an error. */ public static function sanitize( array $def, $value ) { $type = isset( $def['type'] ) ? $def['type'] : 'text'; $value = is_scalar( $value ) ? trim( (string) $value ) : ''; switch ( $type ) { case 'price': if ( '' === $value ) { return ''; } $clean = wc_format_decimal( $value ); if ( '' === $clean || ! is_numeric( $clean ) || (float) $clean < 0 ) { return new WP_Error( 'bdbe_price', __( 'Enter a price of 0 or more, e.g. 19.99.', 'gridsmith' ) ); } return (string) $clean; case 'int': if ( '' === $value ) { return ''; } if ( ! preg_match( '/^-?\d+$/', $value ) ) { return new WP_Error( 'bdbe_int', __( 'Enter a whole number.', 'gridsmith' ) ); } return (string) (int) $value; case 'select': $options = isset( $def['options'] ) ? (array) $def['options'] : array(); if ( ! array_key_exists( $value, $options ) ) { return new WP_Error( 'bdbe_select', __( 'Choose one of the listed values.', 'gridsmith' ) ); } return $value; case 'date': if ( '' === $value ) { return ''; } $parts = explode( '-', $value ); if ( 3 !== count( $parts ) || ! checkdate( (int) $parts[1], (int) $parts[2], (int) $parts[0] ) ) { return new WP_Error( 'bdbe_date', __( 'Enter a date as YYYY-MM-DD.', 'gridsmith' ) ); } return sprintf( '%04d-%02d-%02d', (int) $parts[0], (int) $parts[1], (int) $parts[2] ); case 'textarea': return wp_kses_post( $value ); case 'terms': $names = array_filter( array_map( 'trim', explode( ',', sanitize_text_field( $value ) ) ), 'strlen' ); return implode( ', ', array_unique( $names ) ); case 'readonly': case 'image': return new WP_Error( 'bdbe_readonly', __( 'This column is read-only.', 'gridsmith' ) ); default: return sanitize_text_field( $value ); } } /** * Whether two stored values are the same for a field type. * * @param array $def Field definition. * @param string $a First value. * @param string $b Second value. * @return bool */ public static function same( array $def, $a, $b ) { $a = (string) $a; $b = (string) $b; if ( $a === $b ) { return true; } $type = isset( $def['type'] ) ? $def['type'] : 'text'; if ( in_array( $type, array( 'price', 'int' ), true ) && '' !== $a && '' !== $b && is_numeric( $a ) && is_numeric( $b ) ) { return abs( (float) $a - (float) $b ) < 0.000001; } return false; } /** * Whether a field may be edited on an object. * * @param array $def Field definition. * @param object $object Object. * @return bool */ public static function editable( array $def, $object ) { $type = isset( $def['type'] ) ? $def['type'] : 'text'; if ( in_array( $type, self::READONLY_TYPES, true ) || empty( $def['set'] ) ) { return false; } if ( isset( $def['editable'] ) && is_callable( $def['editable'] ) ) { return (bool) call_user_func( $def['editable'], $object ); } return true; } /** * Column metadata sent to the browser (no callables). * * @param array $fields Field definitions keyed by field key. * @return array */ public static function columns( array $fields ) { $out = array(); foreach ( $fields as $key => $def ) { if ( isset( $def['column'] ) && false === $def['column'] ) { continue; } $type = isset( $def['type'] ) ? $def['type'] : 'text'; $out[] = array( 'key' => (string) $key, 'label' => isset( $def['label'] ) ? (string) $def['label'] : (string) $key, 'type' => $type, 'options' => 'select' === $type && isset( $def['options'] ) ? self::options_list( (array) $def['options'] ) : array(), 'editable' => ! in_array( $type, self::READONLY_TYPES, true ) && ! empty( $def['set'] ), 'default' => ! isset( $def['default'] ) || (bool) $def['default'], 'sortable' => isset( $def['sortable'] ) ? (string) $def['sortable'] : '', 'width' => isset( $def['width'] ) ? (int) $def['width'] : 0, 'hint' => isset( $def['hint'] ) ? (string) $def['hint'] : '', ); } return $out; } /** * Select options as an ordered list (keeps order in JSON). * * @param array $options value => label. * @return array */ private static function options_list( array $options ) { $list = array(); foreach ( $options as $value => $label ) { $list[] = array( 'value' => (string) $value, 'label' => (string) $label, ); } return $list; } }