PHP Closures and Arrow Functions in WordPress Hooks: use Clause, Scope, and Removability

PHP closures — anonymous functions assigned to variables — have been available since PHP 5.3, but their use inside WordPress hooks has some important nuances. A closure passed to add_action() or add_filter() is anonymous and therefore extremely difficult to remove later with remove_action() or remove_filter(), because these functions require the exact same callable reference that was used during registration. Arrow functions (PHP 7.4+) make closures more concise and automatically capture variables from the outer scope without an explicit use clause. Understanding when closures are appropriate, how to remove them when needed, and the scoping rules for use vs arrow functions prevents common bugs in WordPress plugin code.

Problem: A plugin registers a the_content filter using a closure that captures a $wrapper_class variable from configuration. A second plugin needs to remove that filter, but remove_filter( 'the_content', $closure ) silently does nothing because it cannot match the anonymous function reference.

Solution: Store closures in variables or class properties to make them removable. Use arrow functions for simple single-expression callbacks. For callbacks that must be removable by third parties, use named functions or static class methods instead.

<?php
// ── Basic closure with use clause ─────────────────────────────────────
$prefix = 'my-plugin';
add_filter( 'body_class', function ( array $classes ) use ( $prefix ) {
    $classes[] = $prefix . '-active'; // $prefix captured by value
    return $classes;
} );

// ── Arrow function (PHP 7.4+) — auto-captures outer scope ────────────
// No 'use' needed; reads $prefix from the enclosing scope by value
$prefix = 'my-plugin';
add_filter( 'body_class', fn( $classes ) => array_merge( $classes, [ "$prefix-active" ] ) );

// ── Storing a closure so it can be removed ────────────────────────────
class My_Plugin {
    private static \Closure $content_filter;

    public static function register(): void {
        self::$content_filter = function ( string $content ): string {
            return '<div class="my-wrapper">' . $content . '</div>';
        };
        add_filter( 'the_content', self::$content_filter, 20 );
    }

    public static function unregister(): void {
        remove_filter( 'the_content', self::$content_filter, 20 );
    }
}

My_Plugin::register();
// Later — works because we kept the reference:
My_Plugin::unregister();

// ── Use by reference vs by value ──────────────────────────────────────
$counter = 0;

// By value (default): closure captures a COPY — $counter stays 0 outside
add_action( 'wp_head', function () use ( $counter ) {
    $counter++; // modifies local copy only
} );

// By reference: closure modifies the original variable
add_action( 'wp_head', function () use ( &$counter ) {
    $counter++; // modifies $counter in outer scope
} );

// ── Closures in class methods: binding $this ──────────────────────────
class My_Widget_Handler {
    private string $option_key = 'my_widget_data';

    public function register(): void {
        // $this is automatically available inside closures defined in a method
        add_filter( 'widget_title', function ( string $title ): string {
            $data = get_option( $this->option_key ); // $this works here
            return $data['title'] ?? $title;
        } );
    }
}

// ── When to use named functions instead ───────────────────────────────
// Use named functions (not closures) when:
// 1. The filter/action must be removable by third parties
// 2. The callback is long or complex (readability)
// 3. The same callback is added to multiple hooks

NOTE: Arrow functions (fn() =>) are syntactic sugar for closures and share the same removal problem — they are anonymous and cannot be removed unless you store the reference. The key difference from regular closures is scope: arrow functions implicitly capture all variables from the outer scope by value at the time of definition, whereas regular closures require an explicit use clause. Arrow functions cannot use use ( &$ref ) by reference, cannot contain statements (only a single expression), and cannot have a body with return — if your callback needs any of those, use a regular closure or a named function.