Make WordPress Core

Opened 4 months ago

Last modified 3 months ago

#43621 new feature request

Introduce `add_action_once` and `add_filter_once` sugar.

Reported by: soulseekah Owned by:
Milestone: Awaiting Review Priority: normal
Severity: normal Version:
Component: Plugins Keywords: has-patch has-unit-tests dev-feedback
Focuses: Cc:

Description (last modified by SergeyBiryukov)

It is often useful (especially when writing tests for filters, actions) to run a callback only once, regardless of how many times the filter/action is actually applied/done.

add_filter_once( 'test_action_once', '__return_true' );

$this->assertTrue( apply_filters( 'test_action_once', false ) );
$this->assertFalse( apply_filters( 'test_action_once', false ) );

This would allow developers to run anonymous callbacks that remove themselves from the filter after running once. This can currently be done with the following ugly workarounds:

add_action( 'run_many_times', function() {
    // do stuff once and self-destruct
    remove_action( 'run_many_times', current( $GLOBALS['wp_filter'][ current_filter() ]->callbacks[ 10 ] )['function'] );
} );


$once = null;
add_action( 'run_many_times', $null = function() use ( &$once ) {
    // do stuff once and self-destruct
    remove_action( 'run_many_times', $once );
} );

This is not a duplicate of #38743, the concept is different, the naming is the same, yes.

Non-clashing names here?

  • ​add_self_destructing_filter()
  • add_ephemeral_filter()

open to other suggestions :)

Attachments (3)

43621.wp_hook.diff (3.8 KB) - added by soulseekah 4 months ago.
Add WP_Hook::current_callback, WP_Hook::previous_callback (w/ tests)
43621.once.diff (3.4 KB) - added by soulseekah 4 months ago.
add_action_once, add_filter_once (depends on WP_Hook::previous_callback feature)
43621.2.diff (4.9 KB) - added by soulseekah 3 months ago.

Download all attachments as: .zip

Change History (8)

#1 @SergeyBiryukov
4 months ago

  • Description modified (diff)

#2 @johnbillion
4 months ago

This is certainly a pattern that the test suite uses, and I could see a use for it. That said, is it a pattern that's used in non-test code often?

4 months ago

Add WP_Hook::current_callback, WP_Hook::previous_callback (w/ tests)

4 months ago

add_action_once, add_filter_once (depends on WP_Hook::previous_callback feature)

#3 @soulseekah
4 months ago

  • Keywords has-patch has-unit-tests dev-feedback added

Initial implementation and tests for add_action_once, add_filter_once in 43621.once.diff. This depends on two new methods inside WP_Hook, both of which are implemented and tested in 43621.wp_hook.diff

@johnbillion Indeed, my usual use-case for ephemeral callbacks is for writing self-cleaning tests. However, I have recently found myself suppressing wp_mail sending via the phpmailer_init hook for several emails being sent out in a row.

Another practical example, I thought of is to augment the first title on a post archive page:

add_filter_once( 'the_title', 'color_the_title_red' );

This would make the topmost post on the page be output with a red title, while the rest is output in the regular way.

I hope this makes sense.

Special thanks to @hokku @campusboy1987 @denisco @SergeyBiryukov for reviewing and working on this during our online contributor workshop.

#4 @vortfu
4 months ago

I'm not the biggest fan of implementing something like this in the first place over explicitly add/removing callbacks as needed, but as is this patch only supports the use of one "once" callback (per priority), e.g.

add_filter_once( 'test_filter', '__return_false' );
add_filter_once( 'test_filter', '__return_true' );

// $wp_filter['test_filter']->callbacks
// array(1) {
//   [10]=>
//   array(3) {
//     ["__return_false"]=>...
//     ["_remove_filter_once"]=>...
//     ["__return_true"]=>...
//   }
// }

apply_filters( 'test_filter', null ); // true
apply_filters( 'test_filter', null ); // true
// ...
apply_filters( 'test_filter', null ); // true
// etc

_remove_filter_once() could be re-implemented as an anonymous func to ensure it's always added, but then you'd also have to check that the callback was actually added as well so other, non-once callbacks aren't accidentally removed ...

3 months ago


#5 @soulseekah
3 months ago

@vortfu Thanks for looking at this. Indeed, the bug stems from the fact that adding a callback with the same name is not possible. _wp_filter_build_unique_id returns the same ID for it, so _remove_filter_once is not actually added a second time.

could be re-implemented as an anonymous func

PHP 5.2 does not support anonymous functions, unfortunately. Thus the crazy hacking :) (and bugs).

43621.2.diff contains both the 43621.wp_hook.diff patch and fixes on 43621.once.diff.

Note: See TracTickets for help on using tickets.