Oh Dear is the all-in-one monitoring tool for your entire website. We monitor uptime, SSL certificates, broken links, scheduled tasks and more. You'll get a notifications for us when something's wrong. All that paired with a developer friendly API and kick-ass documentation. O, and you'll also be able to create a public status page under a minute. Start monitoring using our free trial now.

A magic memoization function

Original – by Freek Van der Herten – 4 minute read

Last friday Taylor Otwell tweeted an easy to use memoization function called once:

Taylor was kind enough to share the source code behind the function. Because I'd like to use it in our projects I decided to make a small package out of it. I refactored Taylor's code for readability. His original code was a bit more powerful and could handle some more edge cases out of the box.

Usage

The spatie/once package provides you with a once function. It accepts a callable. Here's quick example:

class MyClass
{
    function getNumber()
    {
        return once(function () {
            return rand(1, 10000);
        });
    }
}

No matter how many times you run (new MyClass())->getNumber() inside the same request you'll always get the same number.

The once function will only run once per combination of argument values the containing method receives.

class MyClass
{
    public function getNumberForLetter($letter)
    {
        return once(function () use ($letter) {
            return $letter . rand(1, 10000000);
        });
    }
}

So calling (new MyClass())->getNumberForLetter('A') will always return the same result, but calling (new MyClass())->getNumberForLetter('B') will return something else.

Behind the curtains

Let's go over the code of the once function to learn how all this magic works. In short: it will execute the given callable and save the result in a an array in the __memoized property of the instance once was called in. When we detect that once has already run before, we're just going to return the value stored inside the __memoized array instead of executing the callable again.

The first thing it does is calling debug_backtrace. We'll use the output to determine in which function and class once is called and to get access to the object that function is running in. Yeah, we're already in voodoo-land. The output of the debug_backtrace is passed to a new instance of Backtrace. That class is just a simple wrapper so we can work more easily with the backtrace.

$trace = debug_backtrace(
    DEBUG_BACKTRACE_PROVIDE_OBJECT, 2
)[1];

$backtrace = new Backtrace($trace);

Next, we're going to check if once was called from within an object. If it was called from a static method or outside a class, we just bail out.

if (! $object = $backtrace->getObject()) {
   throw new Exception('Cannot use `once` outside a non-static method of a class');
}

Now that we're certain once is called within an instance of a class we're going to calculate a hash of the backtrace. This hash will be unique per function once was called in and takes into account the values of the arguments that function receives.

$hash = $backtrace->getArgumentHash();

Finally we will check if there's already a value stored for the given hash. If not, then execute the given $callback and store the result in the __memoized array on the object. In the other case just return the value in the __memoized array (the $callback isn't executed).

if (! isset($object->__memoized[$hash])) {
   $result = call_user_func($callback, $backtrace->getArguments());
   $object->__memoized[$hash] = $result;
}

Some things you need to be aware of

Because once will store results on the instance of the object it's called in, you cannot call once outside of on a object or inside a static method. Also, if you need to serialize an object that uses once be sure to unset the __memoized property when once returns objects. A perfect place for unsetting the __memoized would be the __sleep magic method.


If you like the package be sure to check out the framework agnostic and Laravel specific ones we've made before.

Stay up to date with all things Laravel, PHP, and JavaScript.

You can follow me on these platforms:

On all these platforms, regularly share programming tips, and what I myself have learned in ongoing projects.

Every month I send out a newsletter containing lots of interesting stuff for the modern PHP developer.

Expect quick tips & tricks, interesting tutorials, opinions and packages. Because I work with Laravel every day there is an emphasis on that framework.

Rest assured that I will only use your email address to send you the newsletter and will not use it for any other purposes.

Comments

What are your thoughts on "A magic memoization function"?

Comments powered by Laravel Comments
Want to join the conversation? Log in or create an account to post a comment.