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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions dev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
42 changes: 42 additions & 0 deletions src/FastBoot/Plugin/Config/ForgetValueMemo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);

namespace GraphCommerce\FastBoot\Plugin\Config;

/**
* Empties the value memo after a mutable configuration set and after a switch of the
* current store, the two events that change what a read with the current scope answers.
*/
class ForgetValueMemo
{
public function __construct(
private readonly ValueMemo $memo,
) {
}

/**
* @param object $subject
* @param mixed $result
* @return mixed
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function afterSetValue(object $subject, $result)
{
$this->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;
}
}
66 changes: 66 additions & 0 deletions src/FastBoot/Plugin/Config/ValueMemo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);

namespace GraphCommerce\FastBoot\Plugin\Config;

use GraphCommerce\FastBootCache\Model\Feature;
use Magento\Framework\App\Config;
use Magento\Framework\ObjectManager\ResetAfterRequestInterface;

/**
* The values a request read from the system configuration, by scope, scope code and path,
* so a second read of the same value skips the scope resolution and the type lookup. A
* clean of the configuration, a mutable set and a switch of the current store empty it.
*/
class ValueMemo implements ResetAfterRequestInterface
{
private const SWITCH = 'config_memo';

/** @var array<string, mixed> */
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 = [];
}
}
82 changes: 82 additions & 0 deletions src/FastBoot/Test/Unit/Plugin/Config/ValueMemoTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);

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

use GraphCommerce\FastBoot\Plugin\Config\ForgetValueMemo;
use GraphCommerce\FastBoot\Plugin\Config\ValueMemo;
use GraphCommerce\FastBootCache\Model\Feature;
use Magento\Framework\App\Config;
use Magento\Framework\App\Config\MutableScopeConfigInterface;
use PHPUnit\Framework\TestCase;

class ValueMemoTest extends TestCase
{
private int $reads = 0;

private function memo(): ValueMemo
{
$this->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);
}
}
11 changes: 11 additions & 0 deletions src/FastBoot/etc/di.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@
<preference for="Magento\Framework\Config\View" type="GraphCommerce\FastBoot\Model\Config\OpcacheView"/>
<preference for="Magento\Framework\Config\ViewFactory" type="GraphCommerce\FastBoot\Model\Config\OpcacheViewFactory"/>

<!-- The values a request read, by scope, scope code and path: the second read skips the scope resolution and the type lookup. -->
<type name="Magento\Framework\App\Config">
<plugin name="graphcommerce_fastboot_config_value_memo" type="GraphCommerce\FastBoot\Plugin\Config\ValueMemo"/>
</type>
<type name="Magento\Framework\App\Config\MutableScopeConfigInterface">
<plugin name="graphcommerce_fastboot_forget_config_value_memo" type="GraphCommerce\FastBoot\Plugin\Config\ForgetValueMemo"/>
</type>
<type name="Magento\Store\Model\StoreManagerInterface">
<plugin name="graphcommerce_fastboot_forget_config_value_memo" type="GraphCommerce\FastBoot\Plugin\Config\ForgetValueMemo"/>
</type>

<!-- The system configuration of every scope as one opcache PHP array: no decrypt and no unserialise per request. -->
<type name="Magento\Config\App\Config\Type\System">
<plugin name="graphcommerce_fastboot_system_from_file" type="GraphCommerce\FastBoot\Plugin\Config\SystemFromFile"/>
Expand Down