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

Wanted a slick way to generalize class method memoization. Y'all don't even want to know how it works. ? ? pic.twitter.com/xRJAY1C14y — Taylor Otwell (@taylorotwell) November 4, 2016

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.