From 4c9ef7d7122ed0769dce05e022d8b45a71a535a2 Mon Sep 17 00:00:00 2001 From: Paul Hachmang Date: Fri, 25 Sep 2026 00:30:59 +0200 Subject: [PATCH] A configuration value is resolved once per request The second read of a value by scope, scope code and path answers from a request memo, so a page that reads the same settings per card skips the scope resolution and the type lookup for them. A configuration clean, a mutable set and a switch of the current store empty the memo. --- CHANGELOG.md | 4 + dev/README.md | 1 + .../Plugin/Config/ForgetValueMemo.php | 42 ++++++++++ src/FastBoot/Plugin/Config/ValueMemo.php | 66 +++++++++++++++ .../Test/Unit/Plugin/Config/ValueMemoTest.php | 82 +++++++++++++++++++ src/FastBoot/etc/di.xml | 11 +++ 6 files changed, 206 insertions(+) create mode 100644 src/FastBoot/Plugin/Config/ForgetValueMemo.php create mode 100644 src/FastBoot/Plugin/Config/ValueMemo.php create mode 100644 src/FastBoot/Test/Unit/Plugin/Config/ValueMemoTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index a150d45..ce0d926 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- The configuration values a request read are answered from a request memo on the second read, under the `config_memo` switch: no scope resolution and no type lookup per read. + +## Unreleased + - The stock id of the current website as a cache entry in the local files, from the new FastBootInventory module: no sales channel select per request. A change of a stock's sales channels cleans it. ## 0.2.0-rc8 diff --git a/dev/README.md b/dev/README.md index 4dd92de..638a8e8 100644 --- a/dev/README.md +++ b/dev/README.md @@ -30,6 +30,7 @@ Production installs use the default-enabled optimizations. These switches suppor | `tax_rates` | Tax rates and applied rates per rate request from the cache, for requests that are not customer-specific. | Yes | | `customer_groups` | Customer groups by id from the cache, with their excluded websites. | Yes | | `currency_rates` | Currency rates from the cache; a rate import cleans them. | Yes | +| `config_memo` | The configuration values a request read, by scope, scope code and path; a clean, a mutable set and a store switch empty it. | No | | `stock_id` | The stock id of the current website from the cache; a change of a stock's sales channels cleans it. | Yes | These switches do not enable PHP class preload; that requires `opcache.preload` at FPM startup. diff --git a/src/FastBoot/Plugin/Config/ForgetValueMemo.php b/src/FastBoot/Plugin/Config/ForgetValueMemo.php new file mode 100644 index 0000000..6391953 --- /dev/null +++ b/src/FastBoot/Plugin/Config/ForgetValueMemo.php @@ -0,0 +1,42 @@ +memo->forget(); + + return $result; + } + + /** + * @param object $subject + * @param mixed $result + * @return mixed + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSetCurrentStore(object $subject, $result) + { + $this->memo->forget(); + + return $result; + } +} diff --git a/src/FastBoot/Plugin/Config/ValueMemo.php b/src/FastBoot/Plugin/Config/ValueMemo.php new file mode 100644 index 0000000..dc2ddb6 --- /dev/null +++ b/src/FastBoot/Plugin/Config/ValueMemo.php @@ -0,0 +1,66 @@ + */ + private array $values = []; + + public function __construct( + private readonly Feature $feature, + ) { + } + + /** + * @param mixed $path + * @param mixed $scope + * @param mixed $scopeCode + * @return mixed + */ + public function aroundGetValue(Config $subject, callable $proceed, $path = null, $scope = 'default', $scopeCode = null) + { + if (($scopeCode !== null && !is_scalar($scopeCode)) || !$this->feature->on(self::SWITCH)) { + return $proceed($path, $scope, $scopeCode); + } + $key = $scope . "\0" . ($scopeCode ?? '') . "\0" . $path; + if (!array_key_exists($key, $this->values)) { + $this->values[$key] = $proceed($path, $scope, $scopeCode); + } + + return $this->values[$key]; + } + + /** + * @param mixed $result + * @return mixed + */ + public function afterClean(Config $subject, $result) + { + $this->values = []; + + return $result; + } + + public function forget(): void + { + $this->values = []; + } + + public function _resetState(): void + { + $this->values = []; + } +} diff --git a/src/FastBoot/Test/Unit/Plugin/Config/ValueMemoTest.php b/src/FastBoot/Test/Unit/Plugin/Config/ValueMemoTest.php new file mode 100644 index 0000000..da8fb76 --- /dev/null +++ b/src/FastBoot/Test/Unit/Plugin/Config/ValueMemoTest.php @@ -0,0 +1,82 @@ +reads = 0; + $feature = $this->createStub(Feature::class); + $feature->method('on')->willReturn(true); + + return new ValueMemo($feature); + } + + private function read(ValueMemo $memo, string $path, string $scope = 'default', mixed $code = null): mixed + { + return $memo->aroundGetValue($this->createStub(Config::class), function ($path) { + $this->reads++; + + return $path === 'a/null' ? null : strtoupper($path); + }, $path, $scope, $code); + } + + public function testASecondReadOfTheSameValueSkipsTheConfiguration(): void + { + $memo = $this->memo(); + + $this->assertSame('A/B', $this->read($memo, 'a/b', 'store')); + $this->assertSame('A/B', $this->read($memo, 'a/b', 'store')); + $this->assertNull($this->read($memo, 'a/null')); + $this->assertNull($this->read($memo, 'a/null')); + $this->assertSame(2, $this->reads); + } + + public function testTheScopeAndTheCodeAreInTheKey(): void + { + $memo = $this->memo(); + + $this->read($memo, 'a/b', 'store'); + $this->read($memo, 'a/b', 'store', 'second'); + $this->read($memo, 'a/b', 'website'); + + $this->assertSame(3, $this->reads); + } + + public function testACleanAMutableSetAndAStoreSwitchEmptyTheMemo(): void + { + $memo = $this->memo(); + $forget = new ForgetValueMemo($memo); + + $this->read($memo, 'a/b'); + $memo->afterClean($this->createStub(Config::class), null); + $this->read($memo, 'a/b'); + $forget->afterSetValue($this->createStub(MutableScopeConfigInterface::class), null); + $this->read($memo, 'a/b'); + $forget->afterSetCurrentStore(new \stdClass(), null); + $this->read($memo, 'a/b'); + + $this->assertSame(4, $this->reads); + } + + public function testAScopeObjectAsCodeIsNotMemoised(): void + { + $memo = $this->memo(); + + $this->read($memo, 'a/b', 'store', new \stdClass()); + $this->read($memo, 'a/b', 'store', new \stdClass()); + + $this->assertSame(2, $this->reads); + } +} diff --git a/src/FastBoot/etc/di.xml b/src/FastBoot/etc/di.xml index 0bf2428..130c3f6 100644 --- a/src/FastBoot/etc/di.xml +++ b/src/FastBoot/etc/di.xml @@ -22,6 +22,17 @@ + + + + + + + + + + +