Skip to content

Add new filters to the OrderLimiter #41

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,73 @@ add_filter( 'limit_orders_message_placeholders', function ( $placeholders ) {
Now, we can create customer-facing notices like:

> {store_name} is a little overwhelmed right now, but we'll be able to take more orders on {next_interval:date}. Please check back then!

### Dynamically changing limiter behavior

In certain cases, you may want to further customize the logic around _which_ orders count toward the limit or, for example, change the behavior based on time of day. Limit Orders for WooCommerce has you covered:

#### Customize the counting of qualified orders

Sometimes, you only want to limit certain types of orders. Maybe some orders are fulfilled via third parties (e.g. [dropshipping](https://www.liquidweb.com/woocommerce-resource/dropshipping-glossary/)), or perhaps you're willing to bend the limits a bit for orders that contain certain products.

You can customize the logic used to calculate the count via the `limit_orders_pre_count_qualifying_orders` filter:

```php
/**
* Determine how many orders to count against the current interval.
*
* @param bool $preempt Whether the counting logic should be preempted. Returning
* anything but FALSE will bypass the default logic.
* @param OrderLimiter $limiter The current OrderLimiter instance.
*
* @return int The number of orders that should be counted against the limit.
*/
add_filter( 'limit_orders_pre_count_qualifying_orders', function ( $preempt, $limiter ) {
/*
* Do whatever you need to do here to count how many orders count.
*
* Pay close attention to date ranges here, and check out the public methods
* on the Nexcess\LimitOrders\OrderLimiter class.
*/
}, 10, 2 );
```

Please note that the `LimitOrders::count_qualifying_orders()` method (where this filter is defined) is only called in two situations:

1. When a new order is created.
2. If the `limit_orders_order_count` transient disappears.

#### Dynamically change the order limit

If, for example, you want to automatically turn off the store overnight, you might do so by setting the limit to `0` only during certain hours.

You can accomplish this using the `limit_orders_pre_get_remaining_orders` filter:

```php
/**
* Disable the store between 10pm and 8am.
*
* This works by setting the limit on Limit Orders for WooCommerce to zero if
* the current time is between those hours.
*
* @param bool $preempt Whether or not the default logic should be preempted.
* Returning anything besides FALSE will be treated as the
* number of remaining orders that can be accepted.
*
* @return int|bool Either 0 if the store is closed (meaning zero orders remaining)
* or the value of $preempt if Limit Orders should proceed normally.
*/
add_filter( 'limit_orders_pre_get_remaining_orders', function ( $preempt ) {
$open = new \DateTime('08:00', wp_timezone());
$close = new \DateTime('22:00', wp_timezone());
$now = current_datetime();

// We're currently inside normal business hours.
if ( $now >= $open && $now < $close ) {
return $preempt;
}

// If we've gotten this far, turn off ordering.
return 0;
} );
```
28 changes: 28 additions & 0 deletions src/OrderLimiter.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,21 @@ public function get_placeholders( $setting = '', $message = '' ) {
public function get_remaining_orders() {
$limit = $this->get_limit();

/**
* Filter the number of orders remaining for the current interval.
*
* @param bool $preempt Whether or not the default logic should be preempted.
* Returning anything besides FALSE will be treated as the
* number of remaining orders that can be accepted.
* @param OrderLimiter $limiter The current OrderLimiter object.
*/
$remaining = apply_filters( 'limit_orders_pre_get_remaining_orders', false, $this );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I worry about passing $this for the implementation as it requires the developer to explicitly to include OrderLimiter in their own plugin / theme if they want to test it. Instead, is there specific data that we could provide that would allow customization without coupling their code to ours?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is your concern around developers having the explicitly require_once the class definition, or the fact that we're passing the object in its entirety?

If the former, the class would already be loaded into memory as the filter is being applied from within the OrderLimiter object. Additionally, as long as the Limit Orders plugin is active, the autoloader defined in the base plugin file will also be registered.

The reasoning for passing the limiter object rather than individual parameters is that developers may need any number of the properties exposed by the OrderLimiter class — enabled/disabled status, limit, interval length, current + next interval DateTime objects, whether or not there have been orders in the interval, etc. We could either pass a half-dozen or more properties or a single object that exposes well-documented public methods.


// Return early if a non-false value was returned from the filter.
if ( false !== $remaining ) {
return (int) $remaining;
}

// If there are no limits set, return -1.
if ( ! $this->is_enabled() || -1 === $limit ) {
return -1;
Expand Down Expand Up @@ -377,6 +392,19 @@ public function reset_limiter( $previous, $new ) {
* @return int The number of orders that have taken place within the defined interval.
*/
protected function count_qualifying_orders() {
/**
* Replace the logic used to count qualified orders.
*
* @param bool $preempt Whether the counting logic should be preempted. Returning
* anything but FALSE will bypass the default logic.
* @param OrderLimiter $limiter The current OrderLimiter instance.
*/
$count = apply_filters( 'limit_orders_pre_count_qualifying_orders', false, $this );

if ( false !== $count ) {
return (int) $count;
}

$orders = wc_get_orders( [
'type' => wc_get_order_types( 'order-count' ),
'date_created' => '>=' . $this->get_interval_start()->getTimestamp(),
Expand Down
86 changes: 86 additions & 0 deletions tests/OrderLimiterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,46 @@ public function get_remaining_orders_should_return_zero_if_limits_are_met_or_exc
$this->assertSame( 0, ( new OrderLimiter() )->get_remaining_orders() );
}

/**
* @test
* @testdox get_remaining_orders() should be filterable
*/
public function get_remaining_orders_should_be_filterable() {
$instance = new OrderLimiter();
$called = false;

update_option( OrderLimiter::OPTION_KEY, [
'enabled' => true,
'limit' => 5,
] );

add_filter( 'limit_orders_pre_get_remaining_orders', function ( $preempt, $limiter ) use ( $instance, &$called ) {
$this->assertFalse( $preempt, 'The $preempt argument should start as false.' );
$this->assertSame( $instance, $limiter );
$called = true;

return -1;
}, 10, 2 );

$this->assertSame( -1, $instance->get_remaining_orders() );
$this->assertTrue( $called );
}

/**
* @test
* @testdox get_remaining_orders() should be filterable
*/
public function get_remaining_orders_should_cast_the_return_values_as_integers() {
update_option( OrderLimiter::OPTION_KEY, [
'enabled' => true,
'limit' => 5,
] );

add_filter( 'limit_orders_pre_get_remaining_orders', '__return_true' );

$this->assertSame( 1, ( new OrderLimiter() )->get_remaining_orders(), 'TRUE should be cast as 1.' );
}

/**
* @test
* @group Intervals
Expand Down Expand Up @@ -935,4 +975,50 @@ public function count_qualifying_orders_should_not_limit_results() {

$this->assertSame( 5, $method->invoke( $instance ) );
}

/**
* @test
* @testdox count_qualifying_orders() should be filterable
*/
public function count_qualifying_orders_should_be_filterable() {
$instance = new OrderLimiter();
$called = false;
$method = new \ReflectionMethod( $instance, 'count_qualifying_orders' );
$method->setAccessible( true );

update_option( OrderLimiter::OPTION_KEY, [
'enabled' => true,
'limit' => 1,
] );

add_filter( 'limit_orders_pre_count_qualifying_orders', function ( $preempt, $limiter ) use ( $instance, &$called ) {
$this->assertFalse( $preempt );
$this->assertSame( $instance, $limiter );
$called = true;

return 5;
}, 10, 2 );

$this->assertSame( 5, $method->invoke( $instance ) );
$this->assertTrue( $called );
}

/**
* @test
* @testdox Return values from the limit_orders_pre_count_qualifying_orders filter should be cast as integers
*/
public function return_values_from_pre_count_qualifying_orders_should_be_cast_as_int() {
$instance = new OrderLimiter();
$method = new \ReflectionMethod( $instance, 'count_qualifying_orders' );
$method->setAccessible( true );

update_option( OrderLimiter::OPTION_KEY, [
'enabled' => true,
'limit' => 1,
] );

add_filter( 'limit_orders_pre_count_qualifying_orders', '__return_true' );

$this->assertSame( 1, $method->invoke( $instance ) );
}
}