Skip to Content
Course content

73: Building a Simple Plugin System with Hooks

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this exact scenario play out in a dozen different projects. You want your application to be extensible, so you build a "plugin system." You create a way for other parts of the code to "hook" into a process, but then you realize that while your hooks are being triggered, the data isn't actually changing. It's a frustrating bug because the logs show the plugin is running, yet the output remains stubbornly identical to the original.

The Problem: Filters that don't actually filter

// The "broken" Plugin Manager
class PluginManager {
    private $hooks = [];

    public function add_filter($name, $callback) {
        $this->hooks[$name][] = $callback;
    }

    public function apply_filters($name, $value) {
        if (isset($this->hooks[$name])) {
            foreach ($this->hooks[$name] as $callback) {
                // The mistake is right here
                $callback($value); 
            }
        }
        return $value;
    }
}

// Usage
$pm = new PluginManager();
$pm->add_filter('calculate_total', function($price) {
    return $price + 10; // Adding a shipping fee
});

$total = $pm->apply_filters('calculate_total', 100);
echo $total; // Outputs: 100. Wait, what happened to the 10?

If you're looking at that code and thinking, "Why is it still 100?", you've hit on the core misunderstanding of how filters work. In the example above, the $callback($value) line is executing the function, and the function is indeed returning 110. But that returned value is just floating in space—it's not being assigned to anything. The original $value variable remains 100, and that's what gets returned at the end.

Passing the Torch: Maintaining the Value Chain

A filter's job is to take a piece of data, transform it, and hand it off to the next person in line. To fix this, we need to capture the return value of each callback and pass it into the next one. I like to think of it as a relay race; if the runner doesn't actually hand over the baton, the race stops.

public function apply_filters($name, $value) {
    if (isset($this->hooks[$name])) {
        foreach ($this->hooks[$name] as $callback) {
            // We update $value with the result of the callback
            $value = $callback($value); 
        }
    }
    return $value;
}

Now, when the shipping fee plugin returns 110, $value becomes 110. If you have a second plugin that applies a 10% discount, it will receive 110, subtract the discount, and return 99. This chain is what makes a plugin system powerful.

Distinguishing Actions from Filters

Now, not everything in a plugin system should be a filter. Sometimes you don't want to change a value; you just want to trigger an event—like sending an email after an order is placed. We call these "Actions."

The key difference is that an Action doesn't need to return anything. It just does its job and finishes. I usually implement them as two different methods in the manager to keep the intent clear. If you use a filter for an action, you risk a plugin accidentally returning null or a string and breaking your entire data chain.

class PluginSystem {
    private $filters = [];
    private $actions = [];

    public function add_filter($name, $callback) {
        $this->filters[$name][] = $callback;
    }

    public function add_action($name, $callback) {
        $this->actions[$name][] = $callback;
    }

    public function apply_filter($name, $value) {
        foreach ($this->filters[$name] ?? [] as $callback) {
            $value = $callback($value);
        }
        return $value;
    }

    public function do_action($name, ...$args) {
        foreach ($this->actions[$name] ?? [] as $callback) {
            // Actions just execute; we don't care about the return value
            call_user_func($callback, ...$args);
        }
    }
}

By splitting these, you're telling other developers: "Use add_filter if you want to modify the data I'm giving you, and add_action if you just want to know that something happened." It's a small architectural choice that prevents massive headaches as your codebase grows.




📋 Practical Task

Building a Custom Order Processing Pipeline

You are tasked with creating a simple order system. Use the PluginSystem logic discussed in the lesson to implement the following:

  • Create a PluginSystem class that supports both add_filter and add_action.
  • Implement a "Checkout" process that does three things:
    1. Starts with a base price of 50.00.
    2. Applies a filter called 'order_total' (create at least two plugins: one to add a 5.00 service fee and one to apply a 10% discount).
    3. Triggers an action called 'order_completed' (create a plugin that prints "Sending confirmation email to customer...").
  • Ensure the final price is printed to the screen and the action message is displayed.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.