arraypress / wp-conditions
A flexible conditions/rules engine for WordPress with admin UI, REST API search, and extensible condition types.
Requires
- php: >=8.3
- arraypress/wp-abuseipdb: dev-main
- arraypress/wp-accept-language-utils: dev-main
- arraypress/wp-array-utils: dev-main
- arraypress/wp-composer-assets: dev-main
- arraypress/wp-countries: dev-main
- arraypress/wp-email-utils: dev-main
- arraypress/wp-ip-utils: dev-main
- arraypress/wp-ipinfo: dev-main
- arraypress/wp-ipqualityscore: dev-main
- arraypress/wp-proxycheck: dev-main
- arraypress/wp-referrer-utils: dev-main
- arraypress/wp-string-utils: dev-main
- arraypress/wp-user-agent-utils: dev-main
Requires (Dev)
- phpcompatibility/phpcompatibility-wp: ^2.1
- phpunit/phpunit: ^12.0
- squizlabs/php_codesniffer: ^3.13.5
- wp-coding-standards/wpcs: ^3.4
This package is auto-updated.
Last update: 2026-08-24 12:15:12 UTC
README
A flexible conditions/rules engine for WordPress with an admin UI, REST API search, and extensible condition types. Build powerful rule-based systems for discounts, fraud detection, access control, content display, and more.
Features
- Visual Rule Builder - Intuitive admin UI with AND/OR logic groups
- Multiple Field Types - Text, number, select, posts, terms, users, dates, and more
- Built-in Conditions - Ready-to-use conditions for users, posts, dates, requests, EDD, and WooCommerce
- Extensible - Create custom conditions via arrays, classes, or callbacks
- REST API Search - AJAX-powered search for posts, terms, and users
- Type-Safe Comparisons - Proper comparison logic for each field type
Requirements
- PHP 8.2+
- WordPress 6.0+
Easy Digital Downloads and WooCommerce conditions register themselves only when the plugin in question is active, so neither is a dependency.
Built-in Conditions
| Group | Conditions | Registers when |
|---|---|---|
| Core | Date/time, user, post, request, context, email, geo, velocity, payment | Always |
| Services | ProxyCheck, IPinfo, IPQualityScore | An API key is configured |
| WooCommerce | Cart, product, customer, checkout, order, store | WooCommerce is active |
| Easy Digital Downloads | Cart, product, customer, checkout, order, store, commission, recipient | EDD is active |
WooCommerce covers money, items, size and weight, shipping and tax classes, variations and their attributes, stock and backorder policy, and store-wide figures over a date range. EDD covers the same ground plus licensing, recurring, and commissions.
Verifying an Integration
Each condition is a closure over a helper method, and each helper calls into EDD or WooCommerce. PHP checks neither until the closure runs -- which, for a rule that blocks a checkout, is on somebody's purchase. The unit tests cannot close that gap either: they run against stubs, so a stub and a helper can happily agree on a method the plugin has never had.
composer verify
This reads the plugins' own source rather than booting them. It checks that every method, function and option the integration touches exists, and that every helper a condition names is really there. Run it after a WooCommerce or EDD major release. An integration whose plugin is not installed locally is skipped with a note; the library-against-itself half always runs, which is why CI runs it too.
Installation
Install via Composer:
composer require arraypress/wp-conditions
Quick Start
1. Register a Condition Set
use function ArrayPress\Conditions\register_conditions; add_action( 'init', function() { register_conditions( 'discount_rule', [ 'labels' => [ 'singular' => 'Discount Rule', 'plural' => 'Discount Rules', ], 'menu_icon' => 'dashicons-tag', 'menu_parent' => 'edit.php?post_type=shop_order', // Optional: nest under a menu 'conditions' => [ // Built-in conditions 'user_role', 'day_of_week', 'is_logged_in', // Custom condition 'cart_total' => [ 'label' => 'Cart Total', 'group' => 'Cart', 'type' => 'number', 'placeholder' => 'e.g. 100.00', 'arg' => 'cart_total', ], ], ] ); } );
2. Check Conditions
use function ArrayPress\Conditions\check_conditions; // Check and get first matching rule $result = check_conditions( 'discount_rule', [ 'cart_total' => 150.00, 'user_id' => get_current_user_id(), ] ); if ( $result->matched() ) { $rule = $result->get_rule(); $discount = $result->get_rule_meta( '_discount_amount' ); apply_discount( $discount ); }
3. Check All Matching Rules
use function ArrayPress\Conditions\check_all_conditions; $results = check_all_conditions( 'discount_rule', [ 'cart_total' => 200.00, ] ); if ( $results->has_matches() ) { foreach ( $results as $match ) { echo "Matched: " . $match->get_rule_title() . "\n"; } }
Condition Set Configuration
Full Options
register_conditions( 'my_rules', [ 'labels' => [ 'singular' => 'My Rule', 'plural' => 'My Rules', ], 'menu_icon' => 'dashicons-shield', // Dashicon or URL 'menu_parent' => 'tools.php', // Parent menu slug (optional) 'show_in_menu' => true, // Show in admin menu 'capability' => 'manage_options', // Required capability 'description' => 'Configure rule conditions.', // Meta box description 'conditions' => [], // Array of conditions ] );
Field Types
Text
Basic text input with string comparison operators.
'email_domain' => [ 'label' => 'Email Domain', 'group' => 'Customer', 'type' => 'text', 'placeholder' => 'e.g. gmail.com', 'description' => 'Match against the customer email domain.', 'arg' => 'email_domain', ],
Operators: Equals, Does not equal, Contains, Does not contain, Starts with, Ends with, Is empty, Is not empty
Number
Numeric input with mathematical comparison operators.
'order_total' => [ 'label' => 'Order Total', 'group' => 'Order', 'type' => 'number', 'placeholder' => 'e.g. 100.00', 'min' => 0, 'max' => 10000, 'step' => 0.01, 'arg' => 'order_total', ],
Operators: Equal to, Not equal to, Greater than, Less than, Greater or equal to, Less or equal to
Number with Unit
Numeric input paired with a unit selector. Useful for time periods, measurements, etc.
'account_age' => [ 'label' => 'Account Age', 'group' => 'Customer', 'type' => 'number_unit', 'placeholder' => 'e.g. 30', 'min' => 0, 'units' => [ [ 'value' => 'day', 'label' => 'Day(s)' ], [ 'value' => 'week', 'label' => 'Week(s)' ], [ 'value' => 'month', 'label' => 'Month(s)' ], [ 'value' => 'year', 'label' => 'Year(s)' ], ], 'compare_value' => function( $args ) { // Access unit via $args['_unit'] and number via $args['_number'] $unit = $args['_unit'] ?? 'day'; return get_account_age_in_unit( $args['user_id'], $unit ); }, ],
Operators: Equal to, Not equal to, Greater than, Less than, Greater or equal to, Less or equal to
Select (Single)
Dropdown select for choosing one option.
'order_status' => [ 'label' => 'Order Status', 'group' => 'Order', 'type' => 'select', 'multiple' => false, 'placeholder' => 'Select status...', 'options' => [ [ 'value' => 'pending', 'label' => 'Pending' ], [ 'value' => 'processing', 'label' => 'Processing' ], [ 'value' => 'completed', 'label' => 'Completed' ], [ 'value' => 'refunded', 'label' => 'Refunded' ], ], 'arg' => 'order_status', ],
Operators: Is, Is not
Select (Multiple)
Multi-select dropdown for choosing multiple options.
'payment_gateway' => [ 'label' => 'Payment Gateway', 'group' => 'Order', 'type' => 'select', 'multiple' => true, 'placeholder' => 'Select gateways...', 'options' => fn() => get_payment_gateway_options(), // Callback supported 'arg' => 'payment_gateway', ],
Operators: Is any of, Is none of, Is all of
Boolean
Yes/No toggle for true/false conditions.
'is_first_order' => [ 'label' => 'Is First Order', 'group' => 'Customer', 'type' => 'boolean', 'description' => 'Check if this is the customer\'s first order.', 'compare_value' => fn( $args ) => is_first_order( $args['customer_id'] ), ],
Operators: Yes, No
Post
Search and select posts of a specific type.
'purchased_products' => [ 'label' => 'Purchased Products', 'group' => 'Customer', 'type' => 'post', 'post_type' => 'product', // Required: post type to search 'multiple' => true, 'placeholder' => 'Search products...', 'compare_value' => fn( $args ) => get_customer_product_ids( $args['customer_id'] ), ],
Operators (single): Is, Is not
Operators (multiple): Contains any of, Contains none of, Contains all of
Term
Search and select taxonomy terms.
'product_categories' => [ 'label' => 'Product Categories', 'group' => 'Cart', 'type' => 'term', 'taxonomy' => 'product_cat', // Required: taxonomy to search 'multiple' => true, 'placeholder' => 'Search categories...', 'compare_value' => fn( $args ) => get_cart_category_ids(), ],
Operators (single): Is, Is not
Operators (multiple): Contains any of, Contains none of, Contains all of
User
Search and select users, optionally filtered by role.
'assigned_agent' => [ 'label' => 'Assigned Agent', 'group' => 'Ticket', 'type' => 'user', 'role' => 'support_agent', // Optional: filter by role(s) 'multiple' => true, 'placeholder' => 'Search agents...', 'arg' => 'assigned_agent_id', ],
Operators (single): Is, Is not
Operators (multiple): Contains any of, Contains none of, Contains all of
Date
Date picker for date comparisons.
'order_date' => [ 'label' => 'Order Date', 'group' => 'Order', 'type' => 'date', 'description' => 'The date the order was placed.', 'compare_value' => fn( $args ) => get_order_date( $args['order_id'] ), ],
Operators: Is, Is not, Is after, Is before, Is on or after, Is on or before
Time
Time picker for time-of-day comparisons.
'current_time' => [ 'label' => 'Current Time', 'group' => 'Date & Time', 'type' => 'time', 'description' => 'The current time of day.', 'compare_value' => fn( $args ) => current_time( 'H:i' ), ],
Operators: Is, Is not, Is after, Is before
Tags
User-created tags for flexible pattern matching. Great for domains, extensions, prefixes, etc.
'email_domain' => [ 'label' => 'Email Domain', 'group' => 'Customer', 'type' => 'tags', 'placeholder' => 'Type domain and press Enter...', 'description' => 'Match if email ends with any of these domains.', 'operators' => [ 'any_ends' => 'Ends with any of', 'none_ends' => 'Ends with none of', ], 'compare_value' => fn( $args ) => $args['customer_email'], ],
Operator Sets:
- Suffix matching: Ends with any of, Ends with none of
- Prefix matching: Starts with any of, Starts with none of
- Contains matching: Contains any of, Contains none of
- Exact matching: Is any of, Is none of
IP Address
IP address matching with support for exact, CIDR notation, and wildcards.
'customer_ip' => [ 'label' => 'Customer IP', 'group' => 'Request', 'type' => 'ip', 'placeholder' => 'e.g. 192.168.1.0/24 or 10.0.0.*', 'description' => 'Match against the customer IP address.', 'compare_value' => fn( $args ) => $args['ip_address'], ],
Operators: Matches, Does not match
Supported Formats:
- Exact:
192.168.1.1 - CIDR:
192.168.1.0/24 - Wildcard:
192.168.1.*
Email address matching with pattern support.
'customer_email' => [ 'label' => 'Customer Email', 'group' => 'Customer', 'type' => 'email', 'placeholder' => 'e.g. @gmail.com, .edu', 'description' => 'Match customer email patterns.', 'compare_value' => fn( $args ) => $args['email'], ],
Operators: Matches, Does not match
Supported Patterns:
- Full email:
john@example.com - Domain:
@gmail.com - TLD:
.edu - Partial domain:
example.com(matches@example.comand@sub.example.com)
AJAX
Custom AJAX-powered search for any data source.
'discount_code' => [ 'label' => 'Discount Code', 'group' => 'Order', 'type' => 'ajax', 'multiple' => true, 'placeholder' => 'Search discounts...', 'ajax' => function( ?string $search, ?array $ids ): array { // Return array of [ 'value' => '...', 'label' => '...' ] if ( $ids ) { return get_discounts_by_ids( $ids ); } return search_discounts( $search ); }, 'compare_value' => fn( $args ) => $args['applied_discount_ids'], ],
Operators: Same as select (single or multiple based on multiple setting)
Built-in Conditions
User Conditions
| Condition | Description |
|---|---|
user_role |
Match against user role(s) |
is_logged_in |
Check if user is logged in |
user_id |
Match specific user(s) |
user_email |
Match user email |
email_domain |
Match email domain suffix |
user_username |
Match username |
user_registered |
Account age comparison |
user_meta |
Match user meta value |
Date & Time Conditions
| Condition | Description |
|---|---|
day_of_week |
Current day (Monday-Sunday) |
week_of_year |
Week number (1-52) |
day_of_month |
Day of month (1-31) |
current_month |
Current month |
current_date |
Specific date |
current_time |
Time of day |
Post Conditions
| Condition | Description |
|---|---|
post_status |
Post status |
post_type |
Post type |
post_author |
Post author |
post_age |
Time since published |
post_category |
Post categories |
post_tag |
Post tags |
has_term |
Has specific terms |
Request Conditions
| Condition | Description |
|---|---|
current_url |
Current URL |
referrer |
HTTP referrer |
query_var |
URL query parameter |
accept_language |
Browser language |
ip_address |
Visitor IP |
country |
Visitor country |
device_type |
Mobile/Desktop/Bot |
browser |
Browser type |
operating_system |
OS type |
WordPress Context Conditions
| Condition | Description |
|---|---|
is_front_page |
Is site front page |
is_home |
Is blog home |
is_single |
Is single post |
is_page |
Is static page |
is_archive |
Is archive page |
is_search |
Is search results |
is_404 |
Is 404 page |
is_admin |
Is admin area |
is_ajax |
Is AJAX request |
is_rest |
Is REST request |
is_cron |
Is cron job |
EDD Conditions
When Easy Digital Downloads is active, additional conditions are available:
Cart: Total, subtotal, tax, fees, quantity, products, categories, tags, discounts, bundle count, subscription count, license count, renewal count, free item count
Customer: Type (new/returning), order count, total spent, orders/spend in period, email, purchased products/categories/tags, account age, IP count, refund rate
Order: Total, subtotal, tax, discount, status, gateway, currency, products, categories, tags, country, region, city, IP, email, dates, renewal/subscription status
Product: Type, status, categories, tags, sales, earnings, sales/earnings in period
Store: Earnings in period, sales in period, refunds in period, refund rate, tax collected
Checkout: Selected gateway, billing country, billing region
Commission: Amount, rate, status, type, product, categories, tags, recipient user
Recipient: Total/paid/unpaid earnings, sales counts, account age, vendor status, commission rate, payout method
Service Conditions (Third-Party APIs)
Opt-in conditions backed by external lookup services. Each provider's conditions only fire when the matching API key is present in the evaluation $args (e.g. proxycheck_api_key). Responses are cached per request so multiple rules referencing the same provider only spend one API call.
ProxyCheck.io: Risk score, proxy/VPN/Tor detection, country/continent/city, ASN/provider/organisation, network type (Hosting/Residential/Mobile/Business), VPN operator (with sibling-operator matching for shared infra like Mullvad/Hide.me), no-logs / accepts-crypto / accepts-anonymous / free-VPN policies, disposable email check.
IPQualityScore: Fraud score, proxy/VPN/Tor, abuse velocity, recent abuse, bot status, ISP/organisation/ASN, country/region/city, mobile/connection type.
IPInfo: Country/region/city, ASN, organisation, hosting/proxy/VPN/Tor flags, mobile, abuser flag.
MaxMind minFraud: Risk score, IP risk, email/billing/shipping risks, distance signals.
AbuseIPDB: Confidence-of-abuse score, total reports, hosting flag, country.
Square: Risk-evaluation level (low/moderate/high), AVS / CVV codes.
Creating Custom Conditions
Array-Based (Inline)
register_conditions( 'my_rules', [ 'conditions' => [ 'cart_total' => [ 'label' => 'Cart Total', 'group' => 'Cart', 'type' => 'number', 'placeholder' => 'e.g. 100.00', 'min' => 0, 'step' => 0.01, 'description' => 'The shopping cart total.', 'arg' => 'cart_total', // Pull from args 'required_args' => [ 'cart_total' ], // Required args ], 'customer_type' => [ 'label' => 'Customer Type', 'group' => 'Customer', 'type' => 'select', 'options' => [ [ 'value' => 'new', 'label' => 'New Customer' ], [ 'value' => 'returning', 'label' => 'Returning Customer' ], ], 'compare_value' => function( $args ) { // Dynamic comparison value return has_previous_orders( $args['user_id'] ) ? 'returning' : 'new'; }, ], ], ] );
Class-Based
Create reusable conditions by extending the Condition class:
use ArrayPress\Conditions\Condition; class Cart_Total_Condition extends Condition { protected string $name = 'cart_total'; protected string $label = 'Cart Total'; protected string $group = 'Cart'; protected string $type = 'number'; protected ?string $arg = 'cart_total'; protected array $required_args = [ 'cart_total' ]; public function get_operators(): array { return [ '>' => 'Greater than', '<' => 'Less than', '>=' => 'Greater or equal to', '<=' => 'Less or equal to', ]; } public function get_compare_value( array $args ): mixed { // Custom logic to get the comparison value return WC()->cart->get_total( 'edit' ); } public function compare( string $operator, mixed $user_value, mixed $compare_value ): bool { // Custom comparison logic (optional - uses default if not overridden) return parent::compare( $operator, $user_value, $compare_value ); } } // Register register_condition( 'my_rules', Cart_Total_Condition::class );
Adding Conditions to Existing Sets
// Add single condition register_condition( 'discount_rule', 'user_role' ); // Add class-based condition register_condition( 'discount_rule', My_Custom_Condition::class ); // Add inline condition register_condition( 'discount_rule', 'vip_customer', [ 'label' => 'VIP Customer', 'group' => 'Customer', 'type' => 'boolean', 'compare_value' => fn( $args ) => is_vip_customer( $args['user_id'] ), ] );
Matching Conditions
Single Match (First)
$result = check_conditions( 'fraud_rule', [ 'order_total' => 500.00, 'billing_country' => 'US', 'ip_address' => '192.168.1.100', ] ); if ( $result->matched() ) { // Get the matched rule $rule = $result->get_rule(); // WP_Post object $id = $result->get_rule_id(); // Post ID $title = $result->get_rule_title(); // Post title // Get rule meta $action = $result->get_rule_meta( '_fraud_action' ); // Get the matched condition group $group = $result->get_matched_group(); }
All Matches
$results = check_all_conditions( 'discount_rule', [ 'cart_total' => 200.00, 'user_id' => 123, ] ); // Check if any matched if ( $results->has_matches() ) { // Get count $count = $results->count(); // Get all rule IDs $rule_ids = $results->get_rule_ids(); // Get all rule titles $titles = $results->get_rule_titles(); // Get all rule posts $rules = $results->get_rules(); // Get first/last match $first = $results->get_first(); $last = $results->get_last(); // Iterate foreach ( $results as $match ) { $discount = $match->get_rule_meta( '_discount_percent' ); apply_discount( $discount ); } // Filter results $high_priority = $results->filter( function( $match ) { return $match->get_rule_meta( '_priority' ) === 'high'; } ); // Map results $discounts = $results->map( function( $match ) { return $match->get_rule_meta( '_discount_amount' ); } ); }
Custom Operators
Override default operators for a condition:
'priority_level' => [ 'label' => 'Priority Level', 'type' => 'number', 'operators' => [ '>' => 'Higher than', '<' => 'Lower than', '==' => 'Exactly', ], 'arg' => 'priority', ],
Use the Operators class for predefined sets:
use ArrayPress\Conditions\Operators; 'my_condition' => [ 'operators' => Operators::numeric(), // Numeric comparisons 'operators' => Operators::text(), // Text comparisons 'operators' => Operators::boolean(), // Yes/No 'operators' => Operators::date(), // Date comparisons 'operators' => Operators::array_multiple(), // Array containment 'operators' => Operators::tags_ends(), // Suffix matching 'operators' => Operators::ip(), // IP matching 'operators' => Operators::email(), // Email matching ],
Time Periods
Use the Periods class for standardized time units:
use ArrayPress\Conditions\Periods; 'orders_in_period' => [ 'type' => 'number_unit', 'units' => Periods::get_units(), // hour, day, week, month, year // or 'units' => Periods::get_age_units(), // day, week, month, year (no hours) ], // Convert period to seconds $seconds = Periods::to_seconds( 'week', 2 ); // 2 weeks in seconds // Get date range $range = Periods::get_date_range( 'month', 1 ); // Returns: [ 'start' => '2024-01-15 00:00:00', 'end' => '2024-02-15 23:59:59' ]
Hooks and Filters
Actions
// After conditions are saved add_action( 'save_post_{post_type}', function( $post_id, $post ) { $conditions = get_post_meta( $post_id, '_conditions', true ); // Do something with saved conditions }, 20, 2 );
REST API Permissions
// Customize REST API permission check add_filter( 'conditions_rest_permission', function( $allowed ) { return current_user_can( 'edit_posts' ); } );
How Condition Logic Works
Conditions use AND/OR logic:
- Within a group: All conditions must match (AND)
- Between groups: Any group can match (OR)
Example with 2 groups:
Group 1 (AND):
- Cart Total > 100
- User Role = wholesale
OR
Group 2 (AND):
- Cart Total > 500
- Is Logged In = Yes
This matches if:
- Cart is over $100 AND user is wholesale, OR
- Cart is over $500 AND user is logged in
License
GPL-2.0-or-later
Credits
Developed by ArrayPress.