Paul Underwood
PaulundSoftware Engineer
2024-02-11 #php RSS Feed

Improving Function Callbacks

PHP closures are a way of creating anonymous functions. They are useful when you need to pass a function as an argument to another function.

This is a common pattern in PHP, especially when working with arrays. For example, the array_map function takes a callback as its first argument. This callback is then applied to each element in the array.

$numbers = [1, 2, 3, 4, 5];
$doubled = array_map(fn($number) => $number * 2, $numbers);

This is a simple example, but it can quickly become more complex. For example, what if you want to use a method on an object as the callback? You can't use the array_map function directly, as it only accepts a closure.

This is where the Closure::fromCallable method comes in. It allows you to convert a callable into a closure. This means you can use any callable as a callback, not just closures.

class Doubler
{
    public function double($number)
    {
        return $number * 2;
    }
}

$doubler = new Doubler();
$doubled = array_map(Closure::fromCallable([$doubler, 'double']), $numbers);

This is an example of using a class method as a callback, but it can be used with any callable. This makes it a powerful tool for working with functions in PHP.

It's worth noting that this method was added in PHP 7.1, so you'll need to be using at least that version to use it.

The Closure::fromCallable method is a better way of encapsulating function callbacks in PHP which can be used to stop duplicating code and make your code more readable.

Invokable Classes

The above example has a problem and that it's not very readable and we can improve this further by using invokable classes.

class Doubler
{
    public function __invoke($number)
    {
        return $number * 2;
    }
}

$doubler = new Doubler();
$doubled = array_map($doubler, $numbers);

This is a much cleaner way of using the array_map function, and it's easier to understand what's happening. It's also more flexible, as you can use the Doubler class in other contexts as well. You can also reuse this Doubler class in other areas of your code whenever you need to double a number.

This method also allows you to unit test the Doubler class, which is not possible with the closure method. Making this a more reusable and more testable method of coding your application.

This is a simple example to understand the concept of invokable classes but it's strength comes when the callback is more complex and you want to encapsulate the logic into a class.

For example let's say you want to send an email to users, but there's some rules they need to meet before you can send the email. They need to be active, have a valid email address and have not been sent an email in the last 24 hours.

You need to take a list of users and filter out the ones that don't meet these rules.

$users = array_filter($users, function(User $user) {
    if (!$user->isActive()) {
        return false;
    }
    
    if(!$user->hasValidEmail()) {
        return false;
    }
    
    if($user->hasSentEmailInLast24Hours()) {
        return false;
    }
    
    return true;
});

Let's improve this by using an invokeable class.

class ValidUserEmail
{
    public function __invoke(User $user)
    {
        if (!$user->isActive()) {
            return false;
        }
        
        if(!$user->hasValidEmail()) {
            return false;
        }
        
        if($user->hasSentEmailInLast24Hours()) {
            return false;
        }
        
        return true;
    }
}
$users = array_filter($users, new ValidUserEmail());

We can now write some tests for the ValidUserEmail class to make sure it's working correctly. This is not possible with the closure method as it's not possible to test a closure.

class ValidUserEmailTest extends TestCase
{
    public function testValidUserEmail()
    {
        $user = new User();
        $user->setActive(true);
        $user->setEmail('[email protected]');
        $user->setLastEmailSentAt(Carbon::now()->subHours(23));

        $validUserEmail = new ValidUserEmail();
        $this->assertTrue($validUserEmail($user));
    }

    public function testInvalidUserEmail()
    {
        $user = new User();
        $user->setActive(false);
        $user->setEmail('test');
        $user->setLastEmailSentAt(Carbon::now()->subHours(1));

        $validUserEmail = new ValidUserEmail();
        $this->assertFalse($validUserEmail($user));
    }

    public function testUserSentEmailInLast24Hours()
    {
        $user = new User();
        $user->setActive(true);
        $user->setEmail('[email protected]');
        $user->setLastEmailSentAt(Carbon::now()->subHours(23));

        $validUserEmail = new ValidUserEmail();
        $this->assertFalse($validUserEmail($user));
    }

    public function testUserNotActive()
    {
        $user = new User();
        $user->setActive(false);
        $user->setEmail('[email protected]');
        $user->setLastEmailSentAt(Carbon::now()->subHours(23));

        $validUserEmail = new ValidUserEmail();
        $this->assertFalse($validUserEmail($user));
    }
}

Conclusion

In conclusion, the Closure::fromCallable method is a powerful tool for working with functions in PHP. It allows you to convert any callable into a closure, which can then be used as a callback. This makes it easier to work with functions in PHP, and can help to make your code more readable and maintainable.

The __invoke method is a powerful way of encapsulating function callbacks in PHP. It allows you to create classes that can be used as callbacks, which can then be reused in other contexts. This makes your code more flexible and easier to understand, and can help to make your code more testable as well.

By using these methods, you can improve the way you work with functions in PHP, and make your code more maintainable and reusable. This can help to make your code more robust and easier to work with, and can help to make your applications more reliable and easier to maintain.