A memoization function for PHP

Published on 2024-06-01 • Modified on 2024-06-01

This snippet shows how to memoize a pure function result on an object. The code comes from this snippet, thanks to the author, calebporzio. I have slightly modified it and provided a complete working example with time execution traces to show that is works correclty. Check out the links below for more explanations.


<?php

declare(strict_types=1);

namespace App\Controller\Snippet;

use App\Dto\DummyMemoizable;
use Symfony\Component\Stopwatch\Stopwatch;

/**
 * I am using a PHP trait to isolate each snippet in a file.
 * This code should be called from a Symfony controller extending AbstractController (as of Symfony 4.2)
 * or Symfony\Bundle\FrameworkBundle\Controller\Controller (Symfony <= 4.1).
 * Services are injected in the main controller constructor.
 */
trait Snippet303Trait
{
    public function snippet303(): void
    {
        $obj1 = new DummyMemoizable();

        /** @var DummyMemoizable $memo */
        $memo = $this->memoize($obj1);

        /**
         * The compute() function of the DummyMemoizable() object is:
         *
         * $str = $parameter1. $parameter2;
         * usleep(10000); // = 10 milliseconds
         *
         * return $str;
         */
        $timer = new Stopwatch();

        // standard call without memoization
        $event1 = 'std-call';
        $timer->start($event1);
        $res1 = $obj1->compute('foo', 1);
        $timer->stop($event1);
        echo \sprintf('Standard call: %s (%d ms)', $res1, $timer->getEvent($event1)->getDuration()).PHP_EOL;

        // use memoization, the result is slow and stored
        $event2 = 'memo-first-call';
        $timer->start($event2);
        $res2 = $memo->compute('foo', 1);
        $timer->stop($event2);
        echo \sprintf('Memo first call: %s (%d ms)', $res2, $timer->getEvent($event2)->getDuration()).PHP_EOL;

        // with the same arguments, the result is taken from the cache
        $event3 = 'memo-cached-call';
        $timer->start($event3);
        $res3 = $memo->compute('foo', 1);
        $timer->stop($event3);
        echo \sprintf('Memo cached call: %s (%d ms)', $res3, $timer->getEvent($event3)->getDuration()).PHP_EOL;

        // with different arguments a new specific cache is stored
        $event4 = 'memo-new-call';
        $timer->start($event4);
        $res4 = $memo->compute('bar', 2);
        $timer->stop($event4);
        echo \sprintf('Memo new call with other parameters: %s (%d ms)', $res4, $timer->getEvent($event4)->getDuration()).PHP_EOL;

        // That's it! 😁
    }

    public function memoize(object $target): object
    {
        static $memo = new \WeakMap();

        /** @phpstan-ignore-next-line */
        return new class($target, $memo) {
            public function __construct(
                protected object $target,
                protected \WeakMap $memo,
            ) {
            }

            public function __call(string $method, mixed $params): mixed
            {
                $this->memo[$this->target] ??= [];
                $signature = $method.crc32(json_encode($params, JSON_THROW_ON_ERROR));

                return $this->memo[$this->target][$signature]
                    ??= $this->target->$method(...$params);
            }
        };
    }
}

 Run this snippet  More on Stackoverflow   Read the doc  More on the web  Random snippet

  Work with me!