├── .styleci.yml ├── CHANGELOG.md ├── .editorconfig ├── src ├── functions.php ├── Backtrace.php └── Purse.php ├── LICENSE.md ├── composer.json ├── CONTRIBUTING.md └── README.md /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | 3 | linting: true 4 | 5 | disabled: 6 | - single_class_element_per_statement 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All Notable changes to `once` will be documented in this file 4 | 5 | ## 1.1.0 - 2017-08-24 6 | 7 | - add `MemoizeAwareTrait` 8 | 9 | ## 1.0.1 - 2016-11-14 10 | 11 | - fix cacheability of falsy return values 12 | 13 | ## 1.0.0 - 2016-11-07 14 | 15 | - initial release 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 4 9 | indent_style = space 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /src/functions.php: -------------------------------------------------------------------------------- 1 | getObject()) { 15 | throw new Exception('Cannot use `once` outside a non-static method of a class'); 16 | } 17 | 18 | $hash = $backtrace->getHash(); 19 | 20 | $cacheHit = Purse::has($object, $hash); 21 | 22 | if (! $cacheHit) { 23 | $result = call_user_func($callback, $backtrace->getArguments()); 24 | 25 | Purse::set($object, $hash, $result); 26 | } 27 | 28 | return Purse::get($object, $hash); 29 | } 30 | -------------------------------------------------------------------------------- /src/Backtrace.php: -------------------------------------------------------------------------------- 1 | trace = $trace; 13 | } 14 | 15 | public function getArguments(): array 16 | { 17 | return $this->trace['args']; 18 | } 19 | 20 | public function getFunctionName(): string 21 | { 22 | return $this->trace['function']; 23 | } 24 | 25 | /** 26 | * @return mixed 27 | */ 28 | public function getObject() 29 | { 30 | return $this->trace['object']; 31 | } 32 | 33 | public function getHash(): string 34 | { 35 | $normalizedArguments = array_map(function ($argument) { 36 | return is_object($argument) ? spl_object_hash($argument) : $argument; 37 | }, $this->getArguments()); 38 | 39 | return md5($this->getFunctionName().serialize($normalizedArguments)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Spatie bvba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/once", 3 | "description": "A magic memoization function", 4 | "keywords": [ 5 | "spatie", 6 | "once", 7 | "callable", 8 | "memozation", 9 | "cache" 10 | ], 11 | "homepage": "https://github.com/spatie/once", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Freek Van der Herten", 16 | "email": "freek@spatie.be", 17 | "homepage": "https://spatie.be", 18 | "role": "Developer" 19 | } 20 | ], 21 | "require": { 22 | "php": "^7.0" 23 | }, 24 | "require-dev": { 25 | "phpunit/phpunit": "^6.4", 26 | "illuminate/support": "~5.3.0" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "Spatie\\Once\\": "src" 31 | }, 32 | "files": [ 33 | "src/functions.php" 34 | ] 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "Spatie\\Once\\Test\\": "tests" 39 | } 40 | }, 41 | "scripts": { 42 | "test": "vendor/bin/phpunit" 43 | }, 44 | "config": { 45 | "sort-packages": true 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Purse.php: -------------------------------------------------------------------------------- 1 | getNumber()` inside the same request you'll always get the same number. 25 | 26 | Spatie is a webdesign agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource). 27 | 28 | ## Installation 29 | 30 | You can install the package via composer: 31 | 32 | ``` bash 33 | composer require spatie/once 34 | ``` 35 | 36 | ## Usage 37 | 38 | The `once` function accepts a `callable`. 39 | 40 | ```php 41 | class MyClass 42 | { 43 | function getNumber() 44 | { 45 | return once(function () { 46 | return rand(1, 10000); 47 | }); 48 | } 49 | } 50 | ``` 51 | 52 | No matter how many times you run `(new MyClass())->getNumber()` you'll always get the same number. 53 | 54 | The `once` function will only run once per combination of argument values the containing method receives. 55 | 56 | ```php 57 | class MyClass 58 | { 59 | public function getNumberForLetter($letter) 60 | { 61 | return once(function () use ($letter) { 62 | return $letter . rand(1, 10000000); 63 | }); 64 | } 65 | } 66 | ``` 67 | 68 | So calling `(new MyClass())->getNumberForLetter('A')` will always return the same result, but calling `(new MyClass())->getNumberForLetter('B')` will return something else. 69 | 70 | **Note:** In order to use `once` on classes that have the magic method `__get` defined, you will need to either manually define the `__memoized` property on your object, or use the include `MemoizeAwareTrait.` This applies particularly to Laravel Eloquent models. 71 | 72 | ## Behind the curtains 73 | 74 | Let's go over [the code of the `once` function](https://github.com/spatie/once/blob/4954c54/src/functions.php) to learn how all the magic works. 75 | 76 | 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. 77 | 78 | The first thing it does it calling [`debug_backtrace`](http://php.net/manual/en/function.debug-backtrace.php). 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. 79 | 80 | ```php 81 | $trace = debug_backtrace( 82 | DEBUG_BACKTRACE_PROVIDE_OBJECT, 2 83 | )[1]; 84 | 85 | $backtrace = new Backtrace($trace); 86 | ``` 87 | 88 | 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. 89 | 90 | ```php 91 | if (! $object = $backtrace->getObject()) { 92 | throw new Exception('Cannot use `once` outside a class'); 93 | } 94 | ``` 95 | 96 | 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 the values of the arguments that function receives. 97 | 98 | ```php 99 | $hash = $backtrace->getArgumentHash(); 100 | ``` 101 | 102 | 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). 103 | 104 | ```php 105 | if (! isset($object->__memoized[$hash])) { 106 | $result = call_user_func($callback, $backtrace->getArguments()); 107 | $object->__memoized[$hash] = $result; 108 | } 109 | ``` 110 | 111 | ## Caveats 112 | 113 | - you can only use the `once` function in non-static class methods 114 | - if you need to serialize an object that uses `once` be sure to `unset` the `__memoized` property. A perfect place for that would be [the `__sleep` magic method](http://php.net/manual/en/oop4.magic-functions.php) 115 | 116 | ## Changelog 117 | 118 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 119 | 120 | ## Testing 121 | 122 | ``` bash 123 | composer test 124 | ``` 125 | 126 | ## Contributing 127 | 128 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 129 | 130 | ## Security 131 | 132 | If you discover any security related issues, please email freek@spatie.be instead of using the issue tracker. 133 | 134 | ## Postcardware 135 | 136 | You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. 137 | 138 | Our address is: Spatie, Samberstraat 69D, 2060 Antwerp, Belgium. 139 | 140 | We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards). 141 | 142 | ## Credits 143 | 144 | - [Freek Van der Herten](https://github.com/freekmurze) 145 | - [All Contributors](../../contributors) 146 | 147 | Credit for the idea of the `once` function goes to [Taylor Otwell](https://twitter.com/taylorotwell/status/794622206567444481). The code for this package is based upon the code he was kind enough to share with us. 148 | 149 | ## Support us 150 | 151 | Spatie is a webdesign agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource). 152 | 153 | Does your business depend on our contributions? Reach out and support us on [Patreon](https://www.patreon.com/spatie). 154 | All pledges will be dedicated to allocating workforce on maintenance and new awesome stuff. 155 | 156 | ## License 157 | 158 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 159 | --------------------------------------------------------------------------------