The Wonderful World of Callbacks

Anybody that has read any of my previous blog posts or looked at my code samples before should know that I’m a big fan of using callbacks. Whether it’s simply using callbacks with those standard functions that accept them as an argument (such as the functional tripos of array_map(), array_filter() and array_reduce()) or customisable array sorting through usort() and its relatives, building classes with methods that accept callbacks, or the darker magic of binding closures to a class or instance to give it new functionality. I always find that callbacks allow me to build applications with a great deal of flexibility.

Where the logic of the callback is simple (such as filtering an array or iterable), I typically used inline functions; although since the introduction of arrow functions in PHP 7.4, I have been replacing many of my one-liner functions using them instead. If I need something more complex, I might use an anonymous function rather than defining it inline; or if it’s a function that will be called more regularly, or that incorporates business logic that should be maintained separately, then I’ll create it as a named function or method rather than defining it anonymously, so that it’s a separate entity in the codebase, maintained with all the other business logic, and can still be called as a callback.

Any function in PHP – whether user-defined or built-in – can be used as a callback. If I want to trim every value in an array, I can use PHP’s built-in trim() function as a callback. It might not look pretty in our code, but it’s literally just a case of passing the function name as the callback argument:


$array = [
    'Welcome    ',
    '   to     the  ',
    ' world of  ',
    '   PHP',
    '  Callbacks',
];

$result = array_map('trim', $array);

echo implode(' ', $result);

If I also need to trim multiple spaces between words in the elements of that array, something that can’t as easily be done with a single built-in PHP function, then I need a custom callback using both trim() and a preg_replace(); but it’s something that’s still simple enough to do in a single line of code, so I’ll pass an inline function as the callback argument instead:


$result = array_map(
    function (string $value): string {
        return trim(preg_replace('/\s+/', ' ', $value));
    },
    $array
);

(Yes! I know I can eliminate the trim() and just use preg_replace(); but it requires a more complex regular expression, and code readability matters.)

Or replacing that with an arrow functions, as I’d generally do since they were introduced in PHP 7.4:


$result = array_map(
    fn(string $value): string => trim(preg_replace('/\s+/', ' ', $value)),
    $array
);

And for those slightly more complex cases where I might want to create an anonymous function, typically when the logic can’t be reduced to a single line, I can define that function in my code, assigning the function itself to a variable, and then pass that variable as the callback:


$anonymousFunction = function(string $value): string {
    return trim(preg_replace('/\s+/', ' ', $value));
};

$result = array_map(
    $anonymousFunction,
    $array
);

All of these cases are “disposable” callbacks, created dynamically, used, and then discarded once they have served their purpose.
It is also possible to pass additional variable arguments into these callbacks, either passing them into the callback function definition through use, or (in the case of arrow functions) defining those variables in the parent function before using the callback so that they are automagically made available within the scope of that arrow function.


$pregPatternMatch = '/\s+/';
$pregSubstitution = ' ';

$result = array_map(
    function (string $value) use ($pregPatternMatch, $pregSubstitution): string {
        return trim(preg_replace($pregPatternMatch, $pregSubstitution, $value));
    },
    $array
);

or


$result = array_map(
    fn(string $value): string => trim(preg_replace($pregPatternMatch, $pregSubstitution, $value)),
    $array
);

or


$anonymousFunction = function(string $value) use ($pregPatternMatch, $pregSubstitution): string {
    return trim(preg_replace($pregPatternMatch, $pregSubstitution, $value));
};

It becomes more interesting when I want to apply callbacks that I might re-use at various times within the codebase; or where I want to encapsulate some business logic that should be maintainable as a independent entity, separate from where it’s called.

So let’s look at an example with some business logic, a (very simplistic) VAT Calculation. We’ll start by defining our VAT categories and their rates (making use of PHP 8.1 enums):


enum VatCategory {
    case STANDARD;
    case LIVESTOCK;
    case EXEMPT;
    
    public function rate() {
        return match ($this) {
            self::STANDARD => 23.0,
            self::LIVESTOCK => 4.9,
            self::EXEMPT => 0.0,
        }
    }
}

In reality, we’d probably also need a country code check for the different VAT rates of different countries; and to use some form of date checking as well, because some countries have modified their VAT rates at various times over the last two years when they’ve been trying to stimulate the economy; but we’ll keep it simple for demonstration purposes.

and now to define an interface for our calculator:


interface InvoiceCalculator {
    public static function lineTotalWithoutVat(InvoiceLine $line): string;

    public static function lineVat(InvoiceLine $line): string;

    public static function lineTotaWithVat(InvoiceLine $line): string;

    public static function invoiceTotalWithoutVat(Invoice $invoice): string;

    public static function invoiceTotalVat(Invoice $invoice): string;

    public static function invoiceTotalWithVat(Invoice $invoice): string;
}

We want to perform math on monetary values, so we’ll use strings with bcmath to control precision. In reality, I’d probably use a Money object; but I’m trying to keep things relatively simple here.
I’m also using static methods for these calculations, particularly the callbacks, because I want to talk later about some new PHP changes to the way callbacks to static methods are handled.

Now we’ll create our Invoice definition as a simple data object, with no business logic or direct calculation of any kind, just as a series of invoice line items. We’re going to use the business logic maintained independently in the InvoiceCalculator implementation to do all the additional calculations, and provide the values for those totals and VAT:


class InvoiceLine {
    public readonly string $totalWithoutVat;
    public readonly string $vat;
    public readonly string $totalWithVat;
    public function __construct(
        InvoiceCalculator $invoiceCalculator,
        public readonly string $description,
        public readonly int $quantity,
        public readonly float $unitCost,
        public readonly VatCategory $vatCategory = VatCategory::STANDARD
    ) {
        $this->totalWithoutVat = $invoiceCalculator::lineTotalWithoutVat($this);
        $this->vat = $invoiceCalculator::lineVat($this);
        $this->totalWithVat = $invoiceCalculator::lineTotaWithVat($this);
    }
}

class Invoice {
    public readonly string $totalWithoutVat;
    public readonly string $vat;
    public readonly string $totalWithVat;
    public function __construct(
        InvoiceCalculator $invoiceCalculator,
        public readonly array $lines
    ) {
        $this->totalWithoutVat = $invoiceCalculator::invoiceTotalWithoutVat($this);
        $this->vat = $invoiceCalculator::invoiceTotalVat($this);
        $this->totalWithVat = $invoiceCalculator::invoiceTotalWithVat($this);
    }
}

We’re going to inject the Invoice Calculator, and we’re type-hinting it to the interface because that way we can have different calculators for different country’s tax rules, or other variations in business logic (such as 2- or 3-digit “cent” values for different currencies), or different rounding rules. For this example though, I’m only going to implement a single calculator.

So finally the code for that Calculator class itself, with the methods that calculate all the totals and VAT charges for an Invoice, both for the individual line items, and the totals for the Invoice. These are the methods defined in our interface, and that we’re calling inside the InvoiceLine and Invoice constructors to populate the additional properties that are defined explicitly for the class, and aren’t included in the constructor property promotion arguments.


class StdInvoiceCalculator implements InvoiceCalculator {
    public const PRECISION = 2;
    public const VAT_PRECISION = 4;

    public static function lineTotalWithoutVat(InvoiceLine $line): string {
        return bcmul((string) $line->unitCost, (string) $line->quantity, self::PRECISION);
    }

    public static function lineVat(InvoiceLine $line): string {
        return bcmul(
            self::lineTotalWithoutVat($line),
            bcdiv((string) $line->vatCategory->rate(), '100', self::VAT_PRECISION),
            self::PRECISION
        );
    }

    public static function lineTotaWithVat(InvoiceLine $line): string {
        return bcadd(self::lineTotalWithoutVat($line), self::lineVat($line), self::PRECISION);
    }

    private static function totaliser(string $carry, string $value): string {
        return bcadd($carry, $value, self::PRECISION);
    }

    public static function invoiceTotalWithoutVat(Invoice $invoice): string {
        return array_reduce(
            array_map("self::lineTotalWithoutVat", $invoice->lines),
            ["self", "totaliser"],
            '0.0'
        );
    }

    public static function invoiceTotalVat(Invoice $invoice): string {
        return array_reduce(
            array_map("self::lineVat", $invoice->lines),
            ["self", "totaliser"],
            '0.0'
        );
    }

    public static function invoiceTotalWithVat(Invoice $invoice): string {
        return array_reduce(
            array_map("self::lineTotaWithVat", $invoice->lines),
            ["self", "totaliser"],
            '0.0'
        );
    }
}

The real fun with callbacks comes here, where we’re using callbacks for array_map() and array_reduce() in the Invoice total functions. I’m actually using two different approaches to defining the callbacks: either as a string (e.g."self::lineTotalWithoutVat") with the class reference and method names separated by the static function operator ::, or as an array (e.g. ["self", "totaliser"]) with the class reference and method name as two string elements in that array. Both work, and either approach can be used. But more on this anon.

Now we’ll create an invoice populated from the content of a festive, seasonal collection of items that we’ve put in our shopping cart:


$invoiceCalculator = new StdInvoiceCalculator();

$invoice = new Invoice(
    $invoiceCalculator,
    [
        new InvoiceLine($invoiceCalculator, 'Gold Rings', 5, 99.99, VatCategory::STANDARD),
        new InvoiceLine($invoiceCalculator, 'Calling Birds',4, 12.50, VatCategory::LIVESTOCK),
        new InvoiceLine($invoiceCalculator, 'French Hens',3, 15.00, VatCategory::LIVESTOCK),
        new InvoiceLine($invoiceCalculator, 'Turtle Doves',2, 18.00, VatCategory::LIVESTOCK),
        new InvoiceLine($invoiceCalculator, 'Partridge',1, 16.00, VatCategory::LIVESTOCK),
        new InvoiceLine($invoiceCalculator, 'Pear Tree',1, 25.00, VatCategory::EXEMPT),
    ]
);

As each new InvoiceItem is created, its constructor calls the injected InvoiceCalculator to calculate the line item total cost, the VAT based on the item’s VatCategory, and the item cost inclusive of that VAT. When those items are added to the Invoice, the Invoice constructor also calls the the InvoiceCalculator to calculate the totals for all line items, the total VAT, and the total inclusive of VAT using those callbacks.

As the PHP documentation describes it:

Static class methods can also be passed without instantiating an object of that class by either, passing the class name instead of an object at index 0, or passing ‘ClassName::methodName’.

That’s why either a string (e.g."self::lineTotalWithoutVat") with the class reference and method names separated by ::, or an array (e.g. ["self", "totaliser"]) with the class reference and method name as two elements in that array, both work for the callbacks that I pass to array_map() and array_reduce() in the Invoice total functions like


public static function invoiceTotalWithoutVat(Invoice $invoice): string {
    return array_reduce(
        array_map("self::lineTotalWithoutVat", $invoice->lines),
        ["self", "totaliser"],
        '0.0'
    );
}

and I’m sure that a lot of userland code uses one or other of those approaches to specifying callback functions (hopefully consistently, and not a mix of both as I’ve used here for demonstration purposes).

But all that is set to change. These approaches to defining a static callback are due to be deprecated in PHP 8.2. From the next minor release of PHP, this usage will result in Deprecation notices:


Deprecated: Use of "self" in callables is deprecated

Similarly for the use of parent and static.

This is fairly simple to resolve (the RFC recommends how we might do so) by providing a class reference rather than the string reference to self, so (for the “array” approach)


["self", "totaliser"],

should be rewritten as


[self::class, "totaliser"],

For the “string” approach, it’s slightly more awkward, because we need a concatenation (or to switch to using an array instead); but


array_map("self::lineTotalWithoutVat", $invoice->lines),

should become


array_map(self::class . "::lineTotalWithoutVat", $invoice->lines),

Personally, I’d find the “array” approach easier to read than that rather kludgy string concatenation, especially as it retains the :: as a prefix to the method name string making it look more awkward and visually harder to read.

But if both approaches seem ugly, then PHP 8.1 actually introduced a much cleaner approach to setting callbacks with the introduction of first class callable syntax


public static function invoiceTotalWithoutVat(Invoice $invoice): string {
    return array_reduce(
        array_map(self::lineTotalWithoutVat(...), $invoice->lines),
        self::totaliser(...),
        '0.0'
    );
}

I find that much cleaner and easier to read, and the visual presence of (...) tells me intuitively that this is a callback, without my mind having to determine the purpose of the string (or array) in the function call (with the older syntax). While I’m familiar enough with the native PHP functions that use callbacks that it isn’t a big cognitive overload, when userland functions or methods accept callbacks, that cognitive overhead of the older syntax becomes greater (although a good IDE should advise this) because I’m not as familiar with all of those methods or methods. The new syntax eliminates all of that cognitive overhead.


Of course, we could (and should) have defined all the methods in the InvoiceCalculator as instance methods rather than static methods, which would have allowed us to use the “array” approach to defining the callback, replacing the "self" with $this.


public function invoiceTotalWithoutVat(Invoice $invoice): string {
    return array_reduce(
        array_map([$this, "lineTotalWithoutVat"], $invoice->lines),
        [$this, "totaliser"],
        '0.0'
    );
}

But even then, the new first class callable syntax is still a visually cleaner and more readable approach.


public function invoiceTotalWithoutVat(Invoice $invoice): string {
    return array_reduce(
        array_map($this->lineTotalWithoutVat(...), $invoice->lines),
        $this->totaliser(...),
        '0.0'
    );
}

And we can use the new syntax with standard built-in PHP functions (like trim()) when we use them as a callback, to help ease that cognitive load:


$result = array_map(trim(...), $array);

Even anonymous functions can be presented in this way for consistency:


$anonymousFunction = function(string $value): string {
    return trim(preg_replace('/\s+/', ' ', $value));
};

$result = array_map(
    $anonymousFunction(...),
    $array
);

And for inline “one-liners”, using an arrow function is still perfectly clear and concise.


Callbacks can be a very powerful tool adding a lot of flexibility to our code; but the syntax for using them has always felt kludgy and dirty; and the different approaches to defining them is sometimes confusing, and always required a bit of additional mental overhead when we’re reading older code, or something that others have written. At last, first class callable syntax resolves this.
While the deprecations (and the suggested solutions for each approach) in PHP 8.2 do little to provide any real improvement or consistency (with the recommendations to continue using an array, or to use string concatenation); it does give us the impetus to assess callbacks wherever they are used in our codebases, and perhaps switch to using the cleaner, more intuitive first class callable syntax.

This entry was posted in PHP and tagged , , , , , . Bookmark the permalink.

1 Response to The Wonderful World of Callbacks

  1. Pingback: PHP 8.2 – The Release of Deprecations | Mark Baker's Blog

Leave a comment