Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 7 additions & 4 deletions src/FastBoot/Plugin/Store/ScopesCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
{
Expand Down Expand Up @@ -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]
);
}

Expand Down
7 changes: 5 additions & 2 deletions src/FastBoot/Plugin/Store/WebsiteStores.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
{
Expand Down Expand Up @@ -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;
Expand Down
95 changes: 95 additions & 0 deletions src/FastBoot/Test/Unit/Plugin/Store/StoreReinitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);

namespace GraphCommerce\FastBoot\Test\Unit\Plugin\Store;

use GraphCommerce\FastBoot\Plugin\Store\ScopesCache;
use GraphCommerce\FastBoot\Plugin\Store\WebsiteStores;
use GraphCommerce\FastBootCache\Model\Feature;
use Magento\Framework\App\Cache\Type\Config as ConfigCache;
use Magento\Framework\App\CacheInterface;
use Magento\Framework\Serialize\Serializer\Json;
use Magento\Store\App\Config\Source\RuntimeConfigSource;
use Magento\Store\Model\Group;
use Magento\Store\Model\ResourceModel\StoreWebsiteRelation;
use Magento\Store\Model\Store;
use Magento\Store\Model\StoreResolver;
use Magento\Store\Model\Website;
use PHPUnit\Framework\TestCase;

/**
* A new website, group or store runs the store manager's reinit before anything reads
* the scopes again. The reinit cleans the store tags through the config cache type,
* which only drops entries that also carry the config type's tag; an entry without it
* survives, and the scopes reloaded right after come back without the new website.
*/
class StoreReinitTest extends TestCase
{
/** @var array<string, array{data: string, tags: string[]}> */
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));
}
}