From 256ebc19184b0e596246c27e7653e8a85df3c816 Mon Sep 17 00:00:00 2001 From: "Claude (Deployyy workspace)" Date: Tue, 6 Oct 2026 21:37:51 +0000 Subject: [PATCH] A new website is in the scopes the same request reads after it StoreManager::reinitStores() cleans the store, website and group tags through the config cache type, and that tag scope only drops entries that also carry the config type's tag. FASTBOOT_SCOPES and FASTBOOT_WEBSITE_STORES_* carried the store tags only, so they survived the reinit and the scopes reloaded right after came back without the new website. Creating a website then failed Inventory's sales channel validation ("The website with code ... does not exist"). Tag both entries with the config cache tag as well. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + src/FastBoot/Plugin/Store/ScopesCache.php | 11 ++- src/FastBoot/Plugin/Store/WebsiteStores.php | 7 +- .../Unit/Plugin/Store/StoreReinitTest.php | 95 +++++++++++++++++++ 4 files changed, 108 insertions(+), 6 deletions(-) create mode 100644 src/FastBoot/Test/Unit/Plugin/Store/StoreReinitTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index ce0d926..a3851ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- A new website, group or store is in the scopes the same request reads right after it: the scopes and website-stores entries carry the config cache tag, so the store manager's reinit drops them. Before, creating a website from the CLI failed Inventory's sales channel validation ("The website with code … does not exist"). - 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 diff --git a/src/FastBoot/Plugin/Store/ScopesCache.php b/src/FastBoot/Plugin/Store/ScopesCache.php index eefd0a3..28f290b 100644 --- a/src/FastBoot/Plugin/Store/ScopesCache.php +++ b/src/FastBoot/Plugin/Store/ScopesCache.php @@ -4,6 +4,7 @@ namespace GraphCommerce\FastBoot\Plugin\Store; use GraphCommerce\FastBootCache\Model\Feature; +use Magento\Framework\App\Cache\Type\Config as ConfigCache; use Magento\Framework\App\CacheInterface; use Magento\Framework\Serialize\SerializerInterface; use Magento\Store\App\Config\Source\RuntimeConfigSource; @@ -13,9 +14,11 @@ /** * The websites, groups and stores from the cache instead of three selects - * on every request. The tags are the ones a store, website or group save - * cleans through the store manager's reinit, so the entry follows the - * tables; a shop that dumped its scopes into config.php never reads this. + * on every request. The entry carries the config type's tag next to the + * store tags: the store manager's reinit, which a new website, group or store + * runs before anything reads the scopes again, cleans the store tags through + * the config type, and that clean only drops entries that carry its tag. A + * shop that dumped its scopes into config.php never reads this. */ class ScopesCache { @@ -44,7 +47,7 @@ public function aroundGet(RuntimeConfigSource $subject, callable $proceed, $path $this->cache->save( $this->serializer->serialize($data), self::KEY, - [Store::CACHE_TAG, Website::CACHE_TAG, Group::CACHE_TAG] + [ConfigCache::CACHE_TAG, Store::CACHE_TAG, Website::CACHE_TAG, Group::CACHE_TAG] ); } diff --git a/src/FastBoot/Plugin/Store/WebsiteStores.php b/src/FastBoot/Plugin/Store/WebsiteStores.php index ba4a993..df6070e 100644 --- a/src/FastBoot/Plugin/Store/WebsiteStores.php +++ b/src/FastBoot/Plugin/Store/WebsiteStores.php @@ -4,6 +4,7 @@ namespace GraphCommerce\FastBoot\Plugin\Store; use GraphCommerce\FastBootCache\Model\Feature; +use Magento\Framework\App\Cache\Type\Config as ConfigCache; use Magento\Framework\App\CacheInterface; use Magento\Framework\Serialize\SerializerInterface; use Magento\Store\Model\Group; @@ -14,7 +15,9 @@ /** * The stores of a website from the cache: the storeConfig resolver asks for * them on every request with a three-table select. The entry follows the - * store tags, which a store, group or website save cleans. + * store tags, which a store, group or website save cleans, and carries the + * config type's tag so the store manager's reinit, which cleans the store + * tags through the config type, drops it too. */ class WebsiteStores { @@ -49,7 +52,7 @@ public function aroundGetWebsiteStores( $this->cache->save( $this->serializer->serialize($stores), $key, - [Store::CACHE_TAG, Website::CACHE_TAG, Group::CACHE_TAG] + [ConfigCache::CACHE_TAG, Store::CACHE_TAG, Website::CACHE_TAG, Group::CACHE_TAG] ); return $stores; diff --git a/src/FastBoot/Test/Unit/Plugin/Store/StoreReinitTest.php b/src/FastBoot/Test/Unit/Plugin/Store/StoreReinitTest.php new file mode 100644 index 0000000..2886ece --- /dev/null +++ b/src/FastBoot/Test/Unit/Plugin/Store/StoreReinitTest.php @@ -0,0 +1,95 @@ + */ + private array $entries = []; + + private function cache(): CacheInterface + { + $cache = $this->createStub(CacheInterface::class); + $cache->method('load')->willReturnCallback(fn(string $id) => $this->entries[$id]['data'] ?? false); + $cache->method('save')->willReturnCallback(function (string $data, string $id, array $tags = []) { + $this->entries[$id] = ['data' => $data, 'tags' => $tags]; + + return true; + }); + + return $cache; + } + + private function feature(): Feature + { + $feature = $this->createStub(Feature::class); + $feature->method('on')->willReturn(true); + + return $feature; + } + + /** StoreManager::reinitStores(): a matching-any-tag clean through the config type's tag scope. */ + private function reinitStores(): void + { + foreach ([StoreResolver::CACHE_TAG, Store::CACHE_TAG, Website::CACHE_TAG, Group::CACHE_TAG] as $tag) { + foreach ($this->entries as $id => $entry) { + if (in_array($tag, $entry['tags'], true) && in_array(ConfigCache::CACHE_TAG, $entry['tags'], true)) { + unset($this->entries[$id]); + } + } + } + } + + public function testTheScopesReadAfterANewWebsiteComeFromTheTables(): void + { + $plugin = new ScopesCache($this->cache(), new Json(), $this->feature()); + $subject = $this->createStub(RuntimeConfigSource::class); + $tables = ['websites' => ['base' => ['website_id' => '1', 'code' => 'base']]]; + $proceed = static function () use (&$tables): array { + return $tables; + }; + + $plugin->aroundGet($subject, $proceed); + $tables['websites']['wands'] = ['website_id' => '5', 'code' => 'wands']; + $this->reinitStores(); + + self::assertArrayHasKey('wands', $plugin->aroundGet($subject, $proceed)['websites']); + } + + public function testTheStoresOfAWebsiteReadAfterANewStoreComeFromTheTables(): void + { + $plugin = new WebsiteStores($this->cache(), new Json(), $this->feature()); + $subject = $this->createStub(StoreWebsiteRelation::class); + $stores = [['store_id' => '1', 'code' => 'default']]; + $proceed = static function () use (&$stores): array { + return $stores; + }; + + $plugin->aroundGetWebsiteStores($subject, $proceed, 1); + $stores[] = ['store_id' => '2', 'code' => 'second']; + $this->reinitStores(); + + self::assertCount(2, $plugin->aroundGetWebsiteStores($subject, $proceed, 1)); + } +}