Translated labels keyed by operator. */ public static function all(): array { /** * Filters the comparisons a condition may use. * * An operator added here needs two more things to be useful. It must be * evaluated — hook `formpilot_condition_matches`, which runs for any operator * this class does not recognise — and, if the builder is to preview it * live, the same comparison has to exist in * `assets/frontend/js/conditional-logic.js`. Without the second the rule * still works: the server re-evaluates every rule on submit and has the * final say, so the only thing missing is the browser hiding the field * before the visitor presses send. * * @since 1.0.0 * * @param array $operators Translated labels keyed by operator. */ return (array) apply_filters( 'formpilot_condition_operators', array( self::IS => __( 'is', 'devplugins-custom-form-builder' ), self::IS_NOT => __( 'is not', 'devplugins-custom-form-builder' ), self::CONTAINS => __( 'contains', 'devplugins-custom-form-builder' ), self::NOT_CONTAINS => __( 'does not contain', 'devplugins-custom-form-builder' ), self::STARTS_WITH => __( 'starts with', 'devplugins-custom-form-builder' ), self::ENDS_WITH => __( 'ends with', 'devplugins-custom-form-builder' ), self::IS_EMPTY => __( 'is empty', 'devplugins-custom-form-builder' ), self::IS_NOT_EMPTY => __( 'is not empty', 'devplugins-custom-form-builder' ), self::GREATER_THAN => __( 'is greater than', 'devplugins-custom-form-builder' ), self::LESS_THAN => __( 'is less than', 'devplugins-custom-form-builder' ), ) ); } /** * Determines whether an operator is known. * * @param string $operator Operator name. * * @return bool True when the operator exists. */ public static function exists( string $operator ): bool { return array_key_exists( $operator, self::all() ); } /** * Determines whether an operator ignores the compared value. * * The emptiness checks take no second operand, which the builder UI uses to * hide the value input. * * @param string $operator Operator name. * * @return bool True when no value is needed. */ public static function is_unary( string $operator ): bool { return in_array( $operator, array( self::IS_EMPTY, self::IS_NOT_EMPTY ), true ); } /** * Compares a submitted value against an expected one. * * A value from a checkbox group is a list. `is` on a list means "contains * this choice", which is what an author means when they say "when Interests * is Newsletter". * * @param string $operator Operator name. * @param mixed $actual Value the visitor submitted. * @param string $expected Value the condition was configured with. * * @return bool True when the comparison holds. */ public static function evaluate( string $operator, mixed $actual, string $expected ): bool { $list = self::to_list( $actual ); return match ( $operator ) { self::IS => in_array( $expected, $list, true ), self::IS_NOT => ! in_array( $expected, $list, true ), self::CONTAINS => self::any( $list, static fn( string $v ): bool => '' !== $expected && str_contains( $v, $expected ) ), self::NOT_CONTAINS => ! self::any( $list, static fn( string $v ): bool => '' !== $expected && str_contains( $v, $expected ) ), self::STARTS_WITH => self::any( $list, static fn( string $v ): bool => '' !== $expected && str_starts_with( $v, $expected ) ), self::ENDS_WITH => self::any( $list, static fn( string $v ): bool => '' !== $expected && str_ends_with( $v, $expected ) ), self::IS_EMPTY => self::is_blank( $list ), self::IS_NOT_EMPTY => ! self::is_blank( $list ), self::GREATER_THAN => self::compare_number( $list, $expected, true ), self::LESS_THAN => self::compare_number( $list, $expected, false ), default => self::evaluate_custom( $operator, $list, $expected ), }; } /** * Evaluates an operator this class does not implement. * * The counterpart to `formpilot_condition_operators`: registering an operator puts * it in the builder, and answering here decides what it means. Anything * still unrecognised is false, so a rule left behind by a deactivated add-on * stops matching rather than matching everything. * * @param string $operator Operator that was not recognised. * @param array $values Submitted value, normalised to a list. * @param string $expected Value the condition was configured with. * * @return bool True when the condition holds. */ private static function evaluate_custom( string $operator, array $values, string $expected ): bool { /** * Filters the result of an operator the plugin does not implement. * * @since 1.0.0 * * @param bool $matches Whether the condition holds. False unless something says otherwise. * @param string $operator Operator being evaluated. * @param array $values Submitted value, normalised to a list. * @param string $expected Value the condition was configured with. */ return (bool) apply_filters( 'formpilot_condition_matches', false, $operator, $values, $expected ); } /** * Normalises a submitted value to a list of strings. * * @param mixed $value Submitted value. * * @return array The values. */ private static function to_list( mixed $value ): array { if ( is_array( $value ) ) { return array_values( array_map( static fn( $item ): string => is_scalar( $item ) ? (string) $item : '', $value ) ); } return array( is_scalar( $value ) ? (string) $value : '' ); } /** * Determines whether any value satisfies a test. * * @param array $values Values to test. * @param callable $test Test receiving one value. * * @return bool True when at least one value passes. */ private static function any( array $values, callable $test ): bool { foreach ( $values as $value ) { if ( $test( $value ) ) { return true; } } return false; } /** * Determines whether nothing was filled in. * * @param array $values Values to test. * * @return bool True when every value is blank. */ private static function is_blank( array $values ): bool { foreach ( $values as $value ) { if ( '' !== trim( $value ) ) { return false; } } return true; } /** * Compares numerically, ignoring values that are not numbers. * * @param array $values Values to test. * @param string $expected Value to compare against. * @param bool $greater Whether to test for greater than. * * @return bool True when the comparison holds for any value. */ private static function compare_number( array $values, string $expected, bool $greater ): bool { if ( ! is_numeric( $expected ) ) { return false; } $limit = (float) $expected; return self::any( $values, static function ( string $value ) use ( $limit, $greater ): bool { if ( ! is_numeric( $value ) ) { return false; } return $greater ? (float) $value > $limit : (float) $value < $limit; } ); } }