A magic memoization function

Overview

Social Card of Once

A magic memoization function

Latest Version on Packagist Software License Build Status Quality Score StyleCI Total Downloads

This package contains a once function. You can pass a callable to it. Here's quick example:

$myClass = new class() {
    public function getNumber(): int
    {
        return once(function () {
            return rand(1, 10000);
        });
    }
};

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

Are you a visual learner?

Under the hood, this package uses a PHP 8 Weakmap. In this video, you'll see what a weakmap is, together with a nice demo of the package.

Support us

We invest a lot of resources into creating best in class open source packages. You can support us by buying one of our paid products.

We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on our contact page. We publish all received postcards on our virtual postcard wall.

Installation

You can install the package via composer:

composer require spatie/once

Usage

The once function accepts a callable.

$myClass = new class() {
    public function getNumber(): int
    {
        return once(function () {
            return rand(1, 10000);
        });
    }
};

No matter how many times you run $myClass->getNumber() 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
{
    /**
     * It also works in static context!
     */
    public static function getNumberForLetter($letter)
    {
        return once(function () use ($letter) {
            return $letter . rand(1, 10000000);
        });
    }
}

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

Flushing the cache

To flush the entire cache you can call:

Spatie\Once\Cache::getInstance()->flush();

Disabling the cache

In your test you probably don't want to cache values. To disable the cache you can call:

Spatie\Once\Cache::getInstance()->disable();

You can re-enable the cache with

Spatie\Once\Cache::getInstance()->enable();

Under the hood

The once function will execute the given callable and save the result in the $values property of Spatie\Once\Cache. This class is a singleton. When we detect that once has already run before, we're just going to return the value stored inside the $values weakmap 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);

$object = $backtrace->getObject();

Next, we calculate a hash of the backtrace. This hash will be unique per function once was called in and the values of the arguments that function receives.

$hash = $backtrace->getHash();

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 weakmap of Spatie\Once\Cache. In the other case, we just return the value from that cache (the $callback isn't executed).

public function has(object $object, string $backtraceHash): bool
{
    if (! isset($this->values[$object])) {

        return false;
    }

    return array_key_exists($backtraceHash, $this->values[$object]);
}

Changelog

Please see CHANGELOG for more information what has changed recently.

Testing

composer test

Contributing

Please see CONTRIBUTING for details.

Security

If you discover any security related issues, please email [email protected] instead of using the issue tracker.

Credits

And a special thanks to Caneco for the logo

Credit for the idea of the once function goes to Taylor Otwell. The code for this package is based upon the code he was kind enough to share with us.

License

The MIT License (MIT). Please see License File for more information.

Comments
  • Two static functions with the same name

    Two static functions with the same name

    If two static functions have the same name (in two different classes), the once cache is shared between the two resulting in wrong response during the second call (we can invert the two calls, the second one is always the wrong one).

    I've just added a failing test. I read a little bit the code but didn't find the correct way to fix the bug…

    In the once function, the $object variable is different for the two classes but the $hash is the same.

    opened by ThibaudDauce 5
  • Stop using a __memoized property. (Once 2.0?)

    Stop using a __memoized property. (Once 2.0?)

    This tweak just uses a simple "Purse" object to cache all the cache hits instead of putting the values on a hidden property on the object itself. This solves a few problems: 1) don't have to ever think about sleep / wakeup. 2) no need for a MemoizeAware trait to avoid weird overloading errors, etc.

    I'm guessing if merged this would require a 2.0 tag since I removed the trait as it is no longer ever necessary.

    All tests pass without changes.

    If this is accepted I'm considering just including it in Laravel 5.6.

    Let me know what you think :smile:

    opened by taylorotwell 4
  • add static variables of callback to signature

    add static variables of callback to signature

    fixes #59

    The arguments of the surrounding method are now only used in the signature if the closure has arguments (#58) and the static variables use are now part of the cache signature. I've also switched to sha256 to have longer keys and less possible conflict.

    opened by Gummibeer 3
  • Add ability to use once in static methods

    Add ability to use once in static methods

    This PR aims to add ability to use once() in static methods.

    Not sure if there are enough tests, do not hesitate to tell.

    If accepted, I'd love to port these changes for version 1.x as laravel/nova uses version 1.1

    opened by bastien-phi 3
  • Update Dependabot Automation

    Update Dependabot Automation

    Overview

    This PR adds dependabot automation for this repository.

    Description

    This PR updates the workflow that automatically merges dependabot PRs.

    When performing auto-merging of PRs, Github Actions that have a major version update AND a compatibility score of >=90% are automatically merged as way to reduce the manual work of merging PRs that are of no risk of breaking changes. ONLY Github Actions that have a major version update are considered for automatic merging. Other dependencies are excluded.

    id:dependabot-automation/v2

    opened by patinthehat 1
  • Add Dependabot Automation

    Add Dependabot Automation

    This PR adds a dependabot configuration file that enables weekly update checks for all github actions used within workflows in the project. Additionally, it includes a workflow that automatically merges dependabot PRs for minor and patch version updates (major version bumps must be manually merged to avoid breaking changes).

    Using these features will help keep all workflows up to date with a minimal amount of manual intervention necessary.

    Note: this functionality has already been added to the Spatie laravel package skeleton repository.

    opened by patinthehat 1
  • Add support to forget cached objects

    Add support to forget cached objects

    This adds a Cache::forgetObject method so that it is possible to clear the cache for a specific instance or class.

    If you wanted to do this currently then you can either call Cache:flush() which would clear the entire cache or call Cache::forget($objectHash):

    public static function forget(string $objectHash): string
    
    protected static function objectHash($object): string
    

    The first problem is that the objectHash method is protected so you would currently have to either extend the Cache class and implement your own method to get the object hash or to forget an object or duplicate the objectHash logic.

    So a simple solution may be to make objectHash public but then each time you want to forget an object you will have to call objectHash. Instead I have added a convenient forgetObject object that internally gets the object hash and passes it to the forget method.

    This allows you to:

    // Forget instance
    $object = new MyClass();
    $object->getNumber();
    Spatie\Once\Cache::forgetObject($object);
    
    // Forget static method cache passing class reference
    MyClass::getNumberForLetter('A');
    Spatie\Once\Cache::forgetObject(MyClass::class);
    

    The below also currently unintentionally works for class reference as objectHash will return class ref when $object is a string. However, if objectHash logic changed then this could no longer be the case. Whereas, the new forgetObject method would continue to work as expected.

    Spatie\Once\Cache::forget(MyClass::class);
    

    Added test and README update included.

    opened by dmason30 1
  • Allow php8

    Allow php8

    Currently composer reports a problem when using php8:

    Problem 1
        - Root composer.json requires spatie/once ^2.2.0 -> satisfiable by spatie/once[2.2.0].
        - spatie/once 2.2.0 requires php ^7.2 -> your php version (8.0.0RC2) does not satisfy that requirement.
    

    this PR should fix it.

    opened by kurtextrem 1
  • Use an instantiated class in the documentation examples

    Use an instantiated class in the documentation examples

    The examples aren't correct, (new MyClass())->getNumber() isn't repeatable.

    I think each call creates a new instance with a unique hash, but also that they're destroyed and removed from the cache straight away.

    This behaviour appears to go right back to v0.0.1, which makes me think it's a documentation issue rather than an implementation one?

    <?php
    
    require_once 'vendor/autoload.php';
    
    class MyClass
    {
        public function getNumber()
        {
            return once(function () {
                return rand(1, 10000);
            });
        }
    }
    
    var_dump([
        (new MyClass())->getNumber(),
        (new MyClass())->getNumber(),
        (new MyClass())->getNumber(),
    ]); // All different
    
    
    $myClass = new MyClass();
    
    var_dump([
        $myClass->getNumber(),
        $myClass->getNumber(),
        $myClass->getNumber(),
    ]); // All the same
    
    opened by chriscarlisle 1
  • Support global functions

    Support global functions

    A follow up from #41.

    It's a pretty simple change. When once() is called from a global function, it will use file name as a cache identifier (currently it tries to use class name / instance which obviously doesn't exist and errors out).

    I also included a test to make sure it's working.

    opened by sergejostir 1
  • Differentiate between closures

    Differentiate between closures

    This package uses function name in a hash calculation. For closures, function name will always be "namespace{closure}", therefore calculated hash will be always the same, if arguments also match. This raises a problem, since once() will be run only once regardless of the method it is nested in and return the same result across methods.

    What I did here is, that the hashing method checks, if once() was called from within a closure and instead of using generic function name, it uses the line number of once() call.

    Here is an example, how I got an idea for even using once() in a closure:

    public function getSomething($element)
    {
        $getData = function () {
            return once(function () {
                // An expensive function call that does not depend on
                // method's arguments.
            });
        };
    
        retrun $getData()[$element];
    }
    

    If I was to use once() outside the closure, it would take $element argument into consideration, so every time the method would be called with a different argument, once() would call its closure. Now with once() being in a closure, the arguments (there are none in the example) are always the same and the calculated hash will match every time).

    I could of course extract this expensive function call in its own method, but sometimes you would only need that part one time, so declaring a separate method seems overkill and using closure just seems more "right".

    I also included two tests which demonstrate the functionality of this PR.

    Bonus: If you wrap once() in a closure, you can also have multiple such occurances per method giving you different results. Currently you can only have one once() per method because every next one will return you the result of the first one.

    opened by sergejostir 1
  • Bump actions/checkout from 2 to 3

    Bump actions/checkout from 2 to 3

    Bumps actions/checkout from 2 to 3.

    Release notes

    Sourced from actions/checkout's releases.

    v3.0.0

    • Updated to the node16 runtime by default
      • This requires a minimum Actions Runner version of v2.285.0 to run, which is by default available in GHES 3.4 or later.

    v2.5.0

    What's Changed

    Full Changelog: https://github.com/actions/checkout/compare/v2...v2.5.0

    v2.4.2

    What's Changed

    Full Changelog: https://github.com/actions/checkout/compare/v2...v2.4.2

    v2.4.1

    • Fixed an issue where checkout failed to run in container jobs due to the new git setting safe.directory

    v2.4.0

    • Convert SSH URLs like org-<ORG_ID>@github.com: to https://github.com/ - pr

    v2.3.5

    Update dependencies

    v2.3.4

    v2.3.3

    v2.3.2

    Add Third Party License Information to Dist Files

    v2.3.1

    Fix default branch resolution for .wiki and when using SSH

    v2.3.0

    Fallback to the default branch

    v2.2.0

    Fetch all history for all tags and branches when fetch-depth=0

    v2.1.1

    Changes to support GHES (here and here)

    ... (truncated)

    Changelog

    Sourced from actions/checkout's changelog.

    Changelog

    v3.1.0

    v3.0.2

    v3.0.1

    v3.0.0

    v2.3.1

    v2.3.0

    v2.2.0

    v2.1.1

    • Changes to support GHES (here and here)

    v2.1.0

    v2.0.0

    Commits

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    dependencies 
    opened by dependabot[bot] 0
Releases(3.1.0)
Owner
Spatie
We create open source, digital products and courses for the developer community
Spatie
Making multiple identical function calls has the same effect as making a single function call.

Making multiple identical function calls has the same effect as making a single function call.

李铭昕 4 Oct 16, 2021
A magic PHP framework. Build reactive web apps without writing HTML, CSS, or JavaScript! Powered by Tailwind, Alpine, Laravel, & Livewire.

Malzahar A magic PHP framework. Build reactive web apps without writing HTML, CSS, or JavaScript! Powered by Tailwind, Alpine, Laravel, & Livewire. Re

null 26 Nov 17, 2022
WordPress Plugin for Magic

Login by Magic This plugin replaces the standard WordPress login form with one powered by Magic that enables passwordless email magic link login. Plea

Magic 5 Aug 29, 2022
Magic Test allows you to write browser tests by simply clicking around on the application being tested, all without the slowness of constantly restarting the testing environment.

Magic Test for Laravel Magic Test allows you to write browser tests by simply clicking around on the application being tested, all without the slownes

null 400 Jan 5, 2023
The Laravel magic you know, now applied to joins.

The Laravel magic you know, now applied to joins. Joins are very useful in a lot of ways. If you are here, you most likely know about and use them. El

Kirschbaum Development Group, LLC 1k Dec 30, 2022
Create easy (and almost magic) forms

Why Magic Forms? Almost everyday we do forms for our clients, personal projects, etc Sometimes we need to add or remove fields, change validations, st

Martin M. 16 Dec 20, 2022
A simple, safe magic login link generator for Laravel

Laravel Passwordless Login A simple, safe magic login link generator for Laravel This package provides a temporary signed route that logs in a user. W

gro.sv 689 Dec 25, 2022
PHP Magic Number Detector

PHP Magic Number Detector (PHPMND) phpmnd is a tool that aims to help you to detect magic numbers in your PHP code. By default 0 and 1 are not conside

Povilas Susinskas 514 Dec 14, 2022
The magic of Inertia.js with the simplicity of Blade

Laravel Splade The magic of Inertia.js, with the simplicity of Blade. Support Splade. We proudly support the community by developing Laravel packages

Protone Media 702 Dec 29, 2022
Magic admin PHP SDK makes it easy to leverage Decentralized ID tokens to protect routes and restricted resources for your application.

Magic Admin PHP SDK The Magic Admin PHP SDK provides convenient ways for developers to interact with Magic API endpoints and an array of utilities to

magiclabs 17 Jun 26, 2022
Wordpress starting framework for magic websites

Wideo Wordpress starting framework for magic websites. Full documentation: https://github.com/ideonetwork/wideo/wiki Usage Installation for wordpress

Zeranta Digital 4 Dec 13, 2022
Function composition.

igorw/compose Function composition. Allows you to stitch functions together to form a pipeline. This can be useful if you have to transform data in ma

Igor 81 Apr 5, 2022
The VarDumper component provides mechanisms for walking through any arbitrary PHP variable. It provides a better dump() function that you can use instead of var_dump().

VarDumper Component The VarDumper component provides mechanisms for walking through any arbitrary PHP variable. It provides a better dump() function t

Symfony 7.1k Dec 23, 2022
Validates passwords against PHP's password_hash function using PASSWORD_DEFAULT. Will rehash when needed, and will upgrade legacy passwords with the Upgrade decorator.

Password Validator Password Validator validates password_hash generated passwords, rehashes passwords as necessary, and will upgrade legacy passwords.

Jeremy Kendall 142 Dec 25, 2022
The VarDumper component provides mechanisms for walking through any arbitrary PHP variable. It provides a better dump() function that you can use instead of var_dump().

VarDumper Component The VarDumper component provides mechanisms for walking through any arbitrary PHP variable. It provides a better dump() function t

Symfony 7.1k Jan 1, 2023
Appwrite configuration for Cloud Function runtimes settings 🌩

Appwrite Runtimes Appwrite repository for Cloud Function runtimes that contains the configurations and tests for all of the Appwrite runtime environme

Appwrite 39 Nov 22, 2022
Zen Cart® is a full-function e-commerce application for your website.

Zen Cart® - The Art of E-Commerce Zen Cart® was the first Open Source e-Commerce web application to be fully PA-DSS Certified. Zen Cart® v1.5.8 is an

Zen Cart 304 Jan 6, 2023
Search PHP source code for function & method calls, variables, and more from PHP.

Searching PHP source code made easy Search PHP source code for function & method calls, variable assignments, classes and more directly from PHP. Inst

Permafrost Software 22 Nov 24, 2022
A php sharex uploader with discord embed function/twitter card support

Sharex Uploader Simple Sharex Uploader with Discord embed function Download replace your data and upload to your sevrer

Clynt 4 Jan 9, 2022
To run time/IO related unit tests (e.g., sleep function calls, database queries, API calls, etc) faster using Swoole.

To run time/IO related unit tests (e.g., sleep function calls, database queries, API calls, etc) faster using Swoole.

Demin Yin 11 Sep 9, 2022