Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

wallytax's avatar

Memoization

Hi everybody, I hope somebody can help me out with the following.

Yesterday, I've searched the web for information on a thing called "Memoization" in PHP. I'm trying to port a RubyOnRails application to Laravel and in that RubyOnRails application, I created classes to store data that (hardly) change (if they do, I have to change the application's code and redeploy). Thinks of things like countries, supported languages for the application, order statuses, etc.

To keep the post simple, let's say the classes (which I call "Enum" classes) only store a code. The name is derived via Laravel's I18n mechanism.

I've created a base class (BaseEnum) that defines methods to work with these "enums". Then, I created subclasses for each individual case. For simplicity's sake, again I will only show the bare minimum of what I'm trying to deal with.

namespace App\Enums;

class BaseEnum
{
  protected $code;

  // Subclasses can set this to false to avoid sorting by the all() method.
  public static $sort = true;

  // In the real application, other things are possible as well, but this is
  // just to keep it simple and demonstrate the problem.
  public function __construct($code) {
    $this->code = $code;
  }

  // Returns an array of enum instances. The results are sorted by their name
  // unless a subclass sets the static variable $sort to false.
  public static function all() {
    // Convert the string elements of the objects() method to enum instances.
    // *** THIS IS THE LINE THAT I WANT TO HAVE MEMOIZED!!! ***
    $memo = collect(static::objects())->map(fn($i) => new static($i));

    return (static::$sort ? $memo->sortBy(fn($i) => $i->name()) : $memo)->values();
  }

  public function code() {
    return $this->code;
  }

  // Retrieve the translated name via the I18n mechanism of Laravel.
  public function name() {
    return __(static::translationKey() . '.' . $this->code());
  }

  // Subclasses must implement this method and return an array of strings.
  public static function objects() {
    die('Subclasses must implement this method!');
  }

  // Returns the translation key, which is derived from the class' name. Eg. if
  // the class is LanguageEnum, it returns "enums.LanguageEnum".
  public static function translationKey() {
    return 'enums.' . (new \ReflectionClass(static::class))->getShortName();
  }
}

A LanguageEnum class can then be defined like this:

namespace App\Enums;

class LanguageEnum extends BaseEnum
{
  public static $sort = false;

  public static function objects() {
    return ['en', 'nl', 'de', 'fr'];
  }
}

The code of BaseEnum shows one line that I would like to "memoize". In Ruby the all() method would look like this:

def self.all
  @all ||= objects.map { |i| new(i) }
  # sort() is a class method (you don't need brackets in Ruby if there are no
  # arguments)
  sort ? @all.sort_by { |i| i.name } : @all
end

That first line does "all the magic". Without getting to technical, @​all is an instance variable, but if called in a class method (def self. does that), it becomes what I call a class instance variable. The value of that variable is different for each subclass, so LanguageEnum, CountryEnum, OrderStatusEnum would all have their own values for this (not sharing it with each other).

The ||= operator is like using $this->all = $this->all ?? ... in PHP. This is what makes the memoization work. In RubyOnRails, these Enum classes are loaded during start up and maybe sometimes more later, but certainly not for every request.

I tried to port this using static variables, but I noticed that the values are shared with each subclass. I'm afraid there's no "simple" solution for this, so I also looked into storing the results in an "application wide cache", but I'm very new into Laravel, so I don't know (yet) how to do this.

One more thing. You probably might be wondering why I do this, since there's not so much "calculation" going on in the all() class method. In reality, a bit more happens. To me, it seems to be fair to avoid the constant retrieval of the translated names. For now - since I have no solution - I do that. So each time the all() class method is executed, all elements from the objects() class method are converted to instances and sorted (if necessary).

I hope this all made some sense, I tried to give a real-life example for a more generic problem. Believe me, I use this "memoization" all over the place. I appears I am one of the few to do this, since there's not much information on the web for it, certainly not for PHP.

0 likes
8 replies
Sinnbeck's avatar

Sounds like this would fit into config. These are static but can be loaded in from env (but does not have to). You can in theory overwrite then in code, but generally you wouldn't. They are used for defining everything about your app in laravel already and you may simply add your own file(s).

You can also cache them on production so they load faster.

You query them in code like this

$var = config('filename.setting.subsetting');

You could then add your own wrapper for this that sets up the data like you want it to

wallytax's avatar

Thanks for the reply, but for me this is too little information. What would I set in config? A generic cache for all enums? Or one for each individual subclass?

If I could create a "cache" key for the "enums" mechanism and create nested keys for each subclass, that would probably be fine. So the structure of the cache would be something like this (in PHP-notation):

[
  'enums' => [
    'CountryEnum' => [<enum0>, <enum1>, ...]
    ],
    'LanguageEnum' => [<enum0>, <enum1>, ...]
    ]
]

In the application boot sequence (which I know too little of right now), the "enums" key would need to be created (with an empty array I guess).

The BaseEnum class would then need to find the right key from the cache based on the subclass' name. If it does not exist, it should create the key and fill it with the (untranslated) instances. I mention this "untranslated", but it is not really relevant for the problem.

wallytax's avatar

I think I've found the solution myself, but maybe someone could review this code, since I'm unexperienced in PHP. If it is correct, feel free to use it. ;-)

I've created a trait (which I assume to be something similar to Ruby's modules/mix-ins). I've created it in the \app directory of my Laravel project (is there a better place?).

namespace App;

use Illuminate\Support\Facades\Log;

// This trait can be included in classes to enable "memoization".
trait Memoizable
{
    private static $_memoizable = [];

    public static function memoized(string $key, callable $callback) {
        $class = get_called_class();

        if(!isset(static::$_memoizable[$class])) {
            Log::info($class . ': Initializing memoization cache');
            static::$_memoizable[$class] = [];
        }

        if(!isset(static::$_memoizable[$class][$key])) {
            Log::info($class . ': Memoizing \'' . $key . '\'');
            static::$_memoizable[$class][$key] = $callback();
        }

        return static::$_memoizable[$class][$key];
    }
}

This trait can be used as follows (changing the example given earlier in this discussion, removing sorting functionality to keep it as simple as possible):

class MyEnum {
    use \App\Memoizable;

    public static function all() {
        $memo = static::memoized('all', function() {
            return collect(static::objects())->map(fn($i) => new static($i));
        });
        return $memo->values();
    }
}
wallytax's avatar

For anyone still interested in this topic... I found the "spatie/once" plug-in and it looked very promising, but unfortunately, it memoizes per base class. So in my case, the BaseEnum class would memoize the first call that one of it subclasses does, which is not what I want.

I now have a (self made) solution where I use Laravel's Cache class and store the name of the subclass and the given key as one unique key.

public static function all() {
  $memo = Cache::rememberForever(static::class . '/all', function () {
     return collect(static::objects())->map(fn($i) => new static($i));
  });

  return (static::$sort ? $memo->sortBy(fn($i) => $i->name()) : $memo)->values();
}
1 like
wallytax's avatar

Wow, that's a reply on a thread a started some time ago! Thanks for mentioning this, Sacharias! I have looked into it and it looks great! Not sure if I will use it, because I still have my own trait that doesn't need to distinguish between PHP versions, but the idea is the same.

1 like
wallytax's avatar

@Sinnbeck I've re-read the thread and I concluded earlier that it only memoizes in the base class. Not sure if this is still the case however. Can anyone inform me on that?

Please or to participate in this conversation.