Memoize Method Calls in PHP with Cache Decorators

A “memoized” function is a function that only calculates the return value for each combination of arguments once and returns the previously calculated value if the function is called a second time with the same arguments.

Now we can use this cache decorator anywhere where we want to cache results and leave it if not. Also we can have different implementations of ProductRepositoryInterfacewithout duplicating the caching code.

Given any product repository implementation $productRepository, we can add memoization with:

$productRepository = new CachedProductRepository($productRepository);

Generalized solutions

I looked for a general reusable solution and found some that were implemented as higher order functions (i.e. functions that receive other functions as argument and/or return other functions), that map a function to a memoized function.

This is nice for actual functions, but using it with a class is not so practical anymore. We could use the array callback syntax to pass an instance method to the memoize function:

$memoizedLoadProduct = memoize([$productRepository, 'product'])

But then we would have to change client code to use this new function $memoizedLoadProduct, no chance to make use of polymorphism. Of course we could still write the cache decorator and use a memoize function inside but that is going to be more complicated than necessary.

To take “more complicated than necessary” to an extreme level, take a look at dominionenterprises/memoize-php. If you really want persistence and cache lifetime, this seems to be a good solution. If not, this is not for you.

Another interesting solution is amitsnyderman/php-memoize-trait but I would not use it because it’s too much magic: you add a trait to any class which adds a __call() method that lets you call existing methods with an additional underscore to memoize the result:

For this simple example, the overhead of the trait is not really worth it, I would prefer our original cached decorator implementation. But as soon as you have multiple methods to memoize and they have more than one parameter or non-scalar parameters, this should come in handy.

Explanation

All results passed through memoizedCall are stored in a single array, grouped by method name and arguments.

The “magic constant” __FUNCTION__ always contains the current function/method name.

func_get_args() returns an array with all passed arguments, which we serialize to retrieve a distinct string that can be used as array key. Note: this won’t work with values that are not serializable, like resources. You will have to treat those separately.

$this->subject->$methodName(...$args) uses argument unpacking with “…” as a convenient way to call a method with an array of arguments. Before PHP 5.6 one would have needed call_user_func_array() for this.

Is it worth it?

What do we win with this generalized solution? Of course, we have to write less code and it’s still somewhat clear, what the methods are doing. But there are drawbacks:

but less obvious. How does it work? What are the arguments used for? Code is read more often than written, so a few more lines to be more explicit and self-explanatoy are a good investment. That’s still true if there are more arguments. Let’s add a $version parameter to our example:

Still clear and not too complex. Being explicit makes the code more readable.

The existence of this memoizedCall() method might mislead into thoughtlessly using it for anything. Resources that are not serializable are the least problem because this will be noticed immediately. But also big complex object structures can cause trouble because serialization gets imperformant. It’s better to force developers (and if it’s only yourself) to think of the smallest possible identifier and only use memoization where it makes sense.

Conclusion

The Memoize trait is a quick and easy to use way to create cache decorators but quick and easy (not to confuse with “simple”) should not be the our main goal in most cases. If you decide to use it, restrict it to methods with a small signature: just a few scalar values or objects without big hierarchy.

Cache decorators are great to separate caching results from calculation of these results. Use them where caching makes sense and put some thoughts into finding a minimal identifier for the given arguments. No generalized solution can relieve you of this responsibility.

This is CACHE, not memoize. Memoize is for constants, general cache is for variables.
You cannot memoize anything like $id -> $object _from_external_source (that can be modified or not available.
The requirement to memoize is that it only works with pure functions(immutable input, immutable output), not with procedures/methods or anything that touches the network or external memory (globals, $this, closures).
It must be deterministic, you cannot memoize random_bytes or random_int for example. But you can memoize cos, sin, …, you can use memoize for fourier transform (worthy, lengthy but pure).
In other words, if you cannot map a set A to a set B, it is not memoizable.