How to use apply_filters() inside a plugin class?

Your code is good — you are using both apply_filters() and add_filter() correctly.

But the timing/hook does not seem correct.

I think it’s clear from what everyone is saying that it must be an issue of things happening in the wrong order, but don’t plugins always load before the theme?

  • Wrong order: Yes — you most likely called apply_filters() before the filter (change_things()) is added (via add_filter()) or that you instantiated the class Example_Class in the wrong place/hook (i.e. instantiated too early).

  • Don’t plugins always load before the theme? Yes they do, therefore you need to ensure your filter is added on-time — and the earliest hook you’d use to ensure your code runs after the theme is loaded would be after_setup_theme:

    /**
     * Fires after the theme is loaded.
     *
     * @since 3.0.0
     */
    do_action( 'after_setup_theme' );
    

    See this Codex article for a list of the hooks and their order of execution on a WordPress page, both admin and non-admin sides, but for an up-to-date list, try Query Monitor or the other options here.

So for example, if you instantiated the class in plugins_loaded, then the property Example_Class::$stuff would not be filtered by the change_things().

And here are two examples demonstrating how should and should not the class be instantiated:

  • On-time/good instantiation — admin_init runs after after_setup_theme:

    // In main plugin file:
    // Note: I used closure merely for testing purposes.
    
    add_action( 'admin_init', function () {
        require_once '/path/to/class-example-class.php';
        new Example\Example_Class;
        // the $this->stuff in the class would now be the $things returned by
        // change_things()
    } );
    

    And as you may have guessed it, you can use after_setup_theme in place of the admin_init. But in most plugins, they initialize things via the init hook which runs after WordPress setups things like the theme and current user:

    /**
     * Fires after WordPress has finished loading but before any headers are sent.
     *
     * Most of WP is loaded at this stage, and the user is authenticated. WP continues
     * to load on the {@see 'init'} hook that follows (e.g. widgets), and many plugins instantiate
     * themselves on it for all sorts of reasons (e.g. they need a user, a taxonomy, etc.).
     *
     * If you wish to plug an action once WP is loaded, use the {@see 'wp_loaded'} hook below.
     *
     * @since 1.5.0
     */
    do_action( 'init' );
    
  • Too-early instantiation — plugins_loaded runs before after_setup_theme:

    // In main plugin file:
    
    add_action( 'plugins_loaded', function () {
        require_once '/path/to/class-example-class.php';
        new Example\Example_Class;
        // change_things() was not executed
    } );