Flexible API Design: Create Hooks for Your PHP API Pipeline

Tim Hurd
Share

Designing application programming interfaces (APIs) can be a challenging endeavor. Good APIs have simple interfaces that are straightforward and easy to use. Behind this simple interface can be many complex system interactions, and those interactions can really muddy the waters of an otherwise clearly defined endpoint task. Over time, developers may be asked to “tack on” additional business logic for existing endpoints. Then before you know it, a single API call is making interactions with over a dozen systems as part of its main flow.

Wouldn’t it be nice if we could develop a pipeline that’s straightforward but with the capability to add additional tasks later without obscuring the main flow? This article will show you how you can adapt an idea from WordPress, and programming in general, to give your APIs the ability to do more powerful interactions.

What Are Hooks/Actions?

A hook (aka actions/filters) is the name given to events and their related callbacks by the WordPress community. If you have any experience in programming, you might be familiar with callbacks and the publisher–subscriber pattern. During processing, a system may trigger an event which calls zero to many functions subscribed to that event. For instance, in response to loading a page, WordPress makes calls to functions for loading the header, loading a title, listing posts or looking for the right template. These tasks are run without cluttering up the main process of generating a page.

The idea behind hooks is nothing new and wasn’t invented by WordPress. However, WordPress did a great job of implementing them during their server-side page processing lifecycle. This use of hooks, in my opinion, is probably the single greatest feature the platform has. With these hooks, users can write their own functionality — be it plugins or themes — that tie into WordPress and run whatever code you want right when it’s needed. Do you need to alter a header sent to the user? No problem: hook into the wp_headers event and you can alter the headers as you see fit.

Why Use Hooks in an API?

Hooks are good for many things, including triggering some side tasks, calling out to another system through a PHP cURL command, building an object and putting it into a task queue to be picked up by another system later, sending an email, and more. This can all be done without needing to cloud the main flow of a given endpoint (and possibly forcing a new API version in the process).

If the endpoint is for creating a user, we can focus on creating that user record in the database and along the way just call out to whoever is listening during that process. Maybe after creating the user record, we send out an event that says “anyone listening to this, I just created a user and here’s their info”. Maybe some callback functions have subscribed to the event and are listening or maybe none are. The event doesn’t really care.

With this system, we can have our APIs call out to code that may be written at some later time. We can do this without needing to touch the API endpoint code itself. To demonstrate how this might work, let’s change gears and show the basic mechanism of how we can get this started in a PHP API. Do keep in mind that, while we’re using PHP here, we can actually implement similar logic in web applications using other languages.

Building the Basic Mechanisms

To get started, we’ll need to be able to add a hook/action (which I’ll refer to as “hook” from now on). We’ll also need the ability to remove a hook and lastly trigger a hook. Once we define these mechanisms, we just need to make sure they’re included into the API and then locate places in our API where we might want to call these hooks. Below is one way we might want to set this up.

Here’s hooks.php:

// Global array which will hold all of our hooks
// We will reference this array in each function to add/remove/call our hooks
// The code below should also be seen by any callbacks we write for the system later.
$hooks = [];

// Below are global functions that can be seen from our API code
// The add_hook method will allow us to attach a function (callback) to a given event name 
function add_hook($event_name, $callback) {
    global $hooks;

    if ($callback !== null) {
        if ($callback) {
          // We can set up multiple callbacks under a single event name
            $hooks[$event_name][] = $callback;
        }
    }
}

// Super easy to implement, we remove the given hook by its name
function remove_hook($event_name) {
    global $hooks;

    unset($hooks[$event_name]);
}

// When we want to trigger our callbacks, we can call this function 
// with its name and any parameters we want to pass.
function do_hook($event_name, ...$params) {
    global $hooks;

    if (isset($hooks[$event_name])) {
      // Loop through all the callbacks on this event name and call them (if defined that is)
      // As we call each callback, we given it our parameters.
        foreach ($hooks[$event_name] as $function) {
            if (function_exists($function)) {
                call_user_func($function, ...$params);
            }
        }
    }
}

Now that we have our hooks.php file created, we simply need to include it into our API so that these functions can be seen. Once this is done, it’s just a matter of inserting the hooks into our API using do_hook.

As a simple example, let’s assume we have an API for registering a new user with our system. We may have a REST API endpoint called /addUser. In the name of simplicity, let’s also assume that the goal here is to simply insert a new user’s name and age into our database’s users table. Pretty straight forward, right?

// POST endpoint for adding a user (part of a larger API class)
public function addUser($name, $age) {
  if ($this->request->method === 'post') {
    try {
      $this->db->insert('users', ['name' => $name, 'age' => $age]);
      return new Response(200, 'User created successfully!');
    } catch (Exception $e) {
      // Oops, something went wrong.
      // Do some logging or whatever.
    }
  }

  // If not a POST request, return http status 400
  return new Response(400, 'Bad request');
}

The code above is an overly simplistic and generalized view of how we might add a new user. The idea is that, if someone were to call our API’s /addUser endpoint, they would eventually arrive at this function where the name and age of the user is pulled out of the posted data. We first check to make sure they’re posting (as proper REST rules dictate) and then try to insert the user into the users table.

Next, if the user has been inserted successfully, we want to call a hook to let any code listening that a user was created (this is similar to raising an event in other languages).

What to do when requirements change

A few months later, we have our marketing department insisting that, when a new user is created, an email should be sent with the user’s details. We might be inclined to write a helper function into the API, then call it from this endpoint code. Great … if that’s all that was requested. But what if the support team later comes to you and wants you to also add the user to their Zendesk system. So you write another function and tack that call into this endpoint also.

Next thing you know, this endpoint is not only adding a user to our website database, but making calls to functions for sending emails, adding users to Zendesk, Jira and the Microsoft cloud and then dealing with their success/failure results. All this additional stuff is really taking away from the clear point of adding the user to our database. We’d like to call one event and have other code just listen for when a user is created and do their own things — without us needing to modify this endpoint whatsoever. Perhaps no other services care about adding a new user, so no one is called upon to do anything. The endpoint doesn’t have to care. Pretty awesome, right?

Let’s continue our example by giving our hook a name. This is the name that all callback code will need to use to listen. Again, other languages refer to this setup as “events”, so be sure to look it up in your given language to learn more.

We’ll call our hook added_user. Simple and straight to the point, don’t you think? Once we have a name, we can decide where we want to call this hook. I think right after a successful insert would be a good idea:

public function addUser($name, $age) {
  if ($this->request->method === 'post') {
    try {
      $this->db->insert('users', ['name' => $name,  'age' => $age]);
      // Call our new hook and give it the name and age of the user. Anyone listening can then access these params.
      do_hook('added_user', $name, $age);
      return new Response(200, 'User created successfully!');
    } catch (Exception $e) {
      // Oops, something went wrong.
      // Do some logging or whatever.
    }
  }

  return new Response(400, 'Bad request');
}

Now we can have dozens of callback functions listening to the added_user hook or none at all. Maybe we have one callback that’s responsible for inserting the user into Zendesk and another that will take the name and age and generate an email to marketing. This “subscriber” code can live somewhere else in the codebase, as long as it can see the hooks.php code inside the API project. I typically put my callback function in a separate file and include that file into the API as well. Below is one example of a callback that can now take advantage of this new hook we created:

function sendContactToMarketing($name, $age) {
  // Do email stuff
}

add_hook('added_user', 'sendContactToMarketing');

Where Can We Place These Hooks?

In the code above, we demonstrate using a hook in a single endpoint. This hook is only triggered when the /addUser endpoint is called, and only after the insert succeeds. But this isn’t the only place you can use these hooks. Perhaps you have some routing code in your API class that runs through checking if an API key is valid or that you even received a certain type of request.

You could put a hook in at a global level within the API so that every request triggers it. Then, at some later time, someone could write a logger, attach it to the api_init hook you created and suddenly start logging all requests made to the API — again, not touching the main API class code. That same hook may also have an additional callback attached that would check for API abuse and report it to your Information Technology department if it starts seeing someone slamming your API with requests.

The image below is a diagram of how this all looks architecturally.

Diagram of Hooks In API

Since you can use this mechanism in multiple locations, you can even have hooks called at the beginning, middle and at the end of an API endpoint. You could also have hooks at various points of the entire API lifecycle of handling a request. It’s really up to your design on where you insert these do_hook calls.

Best Practices

Let’s now cover some best practices for you and your developers to follow.

Tip 1: Keep your hooks lean and mean

One thing to keep in mind is that these hooks still do call out to code that will execute in the single thread. Unless you trigger something in your callback which kicks the task to some background process or other service, the API will still be running this extra code (when the hook is triggered). This means that we should do our best to keep any callback code lean and mean. Long-running callback code is going to slow down the endpoints or the API as a whole.

Tip 2: Make each callback isolated and simple to debug

However, with the way this structure is designed, adding and removing callback functions for your hooks is easy to do and debugging is just as easy. Find which callback is the offender (maybe have each callback log some data) and find out where it’s getting stuck. Then simply don’t have it subscribe to the hook until the bug is fixed or work through the callback code, again not touching anything within the endpoint/API code and not preventing your API code from running in production.

Tip 3: Think about performance and don’t abuse a hook

It’s also important to be mindful of how many callbacks you attach to your hooks. A handful of quick callbacks is fine, but 100 callbacks on a single hook with each taking one second to execute can really be a drag on your API. We want fast API calls, and each callback can easily drag on response times. Again, if you find a callback slow, throw the task out to a background process or a queue system to be picked up by another service later. I often use jobs in systems like Laravel to handle such tasks. Maybe add the user task to a Laravel job queue and continue on with API processing.

Tip 4: Stay in touch with your dev community

Lastly, make sure you stay in touch with your developers who might be using these hooks. It’s common that developers using your hooks, and writing callbacks, aren’t the same people who created the hook in the API to begin with. As your API matures, you might start seeing requests to add more hooks in different places and with finer granularity. They might ask for before/after hooks that can be used to trigger actions before a user is inserted as well as after. They may also ask for additional info to be passed to their callbacks (not only their name and age but also the new ID of the inserted user, for example). Take these requests as a good sign that your devs find the mechanism useful and see the potential of expanding your API’s impact in related systems. It really feels good to have a system that’s so easy to “hook” into and execute a small piece of code to have a large impact.

With this approach, the sky’s the limit on what you can have your API do. This all can be achieved while keeping your endpoint’s main flow clear and free from noise when dealing with other systems.

Conclusion

In this article, we discussed what hooks/actions are and how they might be used. We gave some example PHP code that you can use in your PHP API to implement “hooking” and how a callback might be used to tie into that hook. We also discussed adding hooks on a general API level (more globally for all requests) as well as in endpoints. In addition to that, we also talked a bit about some of the drawbacks of this system and that it’s a good idea to keep your callbacks lean and mean. But in the event you do have a long-running callback, we mentioned a few strategies for dealing with such processes to prevent them from impacting your API pipeline.

If you implement this system, you too can gain some of the greatest functionality that the WordPress community (and programmers in general) have enjoyed for years. You’ll also save yourself a lot of time and headaches from having to modify your API code directly, and you’ll be able to focus on small callback code instead. You can also add and remove callbacks that may be entire integrations with other systems. All this functionality — and not once having to republish your API pipeline code! That’s a pretty good deal, right? With this system, I’ve managed to do some simple integrations in a day, and now you can do it too.

Thanks for reading!