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
@@ -1,5 +1,9 @@
# Changelog

## 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

- Tax rates, customer groups and currency rates as cache entries in the local files: no select per request for them. A tax rule, rate, class or group save cleans the tax entries; a rate import cleans the currency entries.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ composer require graphcommerce/magento-fast-boot
| FastBoot | [Installation and deployment](src/FastBoot/README.md) |
| FastBootCache | [Cache configuration and behavior](src/FastBootCache/README.md) |
| FastBootGraphQl | [GraphQL optimizations](src/FastBootGraphQl/README.md) |
| FastBootInventory | [The stock id of a website from the cache](src/FastBootInventory/README.md) |
| FastBootPreload | [Class preloading](src/FastBootPreload/README.md) |

## Online benchmarks
Expand Down
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@
"src/FastBootCache/registration.php",
"src/FastBoot/registration.php",
"src/FastBootPreload/registration.php",
"src/FastBootGraphQl/registration.php"
"src/FastBootGraphQl/registration.php",
"src/FastBootInventory/registration.php"
],
"psr-4": {
"GraphCommerce\\FastBootCache\\": "src/FastBootCache/",
"GraphCommerce\\FastBoot\\": "src/FastBoot/",
"GraphCommerce\\FastBootPreload\\": "src/FastBootPreload/",
"GraphCommerce\\FastBootGraphQl\\": "src/FastBootGraphQl/"
"GraphCommerce\\FastBootGraphQl\\": "src/FastBootGraphQl/",
"GraphCommerce\\FastBootInventory\\": "src/FastBootInventory/"
}
},
"archive": {
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 |
| `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
2 changes: 1 addition & 1 deletion src/FastBootCache/Model/Feature.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
class Feature
{
private ?array $switches = null;
private const CACHED = ['schema_array' => true,'cache_files' => true,'system_config_array' => true,'view_config' => true,'parsed_queries' => true,'validated_queries' => true,'scopes_cache' => true,'website_stores' => true,'deploy_config_unchanged' => true,'placeholder_url' => true,'guest_tax_factor' => true,'tax_rates' => true,'customer_groups' => true,'currency_rates' => true];
private const CACHED = ['schema_array' => true,'cache_files' => true,'system_config_array' => true,'view_config' => true,'parsed_queries' => true,'validated_queries' => true,'scopes_cache' => true,'website_stores' => true,'deploy_config_unchanged' => true,'placeholder_url' => true,'guest_tax_factor' => true,'tax_rates' => true,'customer_groups' => true,'currency_rates' => true,'stock_id' => true];

public function __construct(
private readonly DeploymentConfig $deploymentConfig,
Expand Down
1 change: 1 addition & 0 deletions src/FastBootCache/Model/Tag.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ final class Tag
{
public const TAX = 'FASTBOOT_TAX';
public const CURRENCY = 'FASTBOOT_CURRENCY';
public const STOCK = 'FASTBOOT_STOCK';
}
31 changes: 31 additions & 0 deletions src/FastBootInventory/Plugin/ForgetStockIds.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);

namespace GraphCommerce\FastBootInventory\Plugin;

use GraphCommerce\FastBootCache\Model\Tag;
use Magento\Framework\App\CacheInterface;

/**
* Cleans the stock id entries after a write of the sales channel links of a stock.
*/
class ForgetStockIds
{
public function __construct(
private readonly CacheInterface $cache,
) {
}

/**
* @param object $subject
* @param mixed $result
* @return mixed
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function afterExecute(object $subject, $result)
{
$this->cache->clean([Tag::STOCK]);

return $result;
}
}
48 changes: 48 additions & 0 deletions src/FastBootInventory/Plugin/StockIdFromCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);

namespace GraphCommerce\FastBootInventory\Plugin;

use GraphCommerce\FastBootCache\Model\Feature;
use GraphCommerce\FastBootCache\Model\Tag;
use Magento\Framework\App\Cache\Type\Config as ConfigCache;
use Magento\Framework\App\CacheInterface;
use Magento\Framework\App\RequestInterface;
use Magento\InventoryCatalog\Model\GetStockIdForCurrentWebsite;
use Magento\Store\Model\StoreManagerInterface;

/**
* The stock id of the current website as a cache entry, keyed by the website code the
* resolver reads from the request's store.
*/
class StockIdFromCache
{
private const SWITCH = 'stock_id';
private const KEY = 'FASTBOOT_STOCK_ID_';

public function __construct(
private readonly CacheInterface $cache,
private readonly Feature $feature,
private readonly StoreManagerInterface $storeManager,
private readonly RequestInterface $request,
) {
}

public function aroundExecute(GetStockIdForCurrentWebsite $subject, callable $proceed): int
{
if (!$this->feature->on(self::SWITCH)) {
return $proceed();
}
$store = $this->storeManager->getStore($this->request->getParam('store'));
$key = self::KEY . $this->storeManager->getWebsite($store->getWebsiteId())->getCode();
$cached = $this->cache->load($key);
$entry = is_string($cached) ? json_decode($cached, true) : null;
if (is_array($entry) && isset($entry['id'])) {
return (int)$entry['id'];
}
$stockId = $proceed();
$this->cache->save(json_encode(['id' => $stockId]), $key, [ConfigCache::CACHE_TAG, Tag::STOCK]);

return $stockId;
}
}
10 changes: 10 additions & 0 deletions src/FastBootInventory/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# GraphCommerce_FastBootInventory

The stock id of the current website from the cache. Core resolves it with two selects on every
request that loads a product collection.

[`Plugin/StockIdFromCache`](Plugin/StockIdFromCache.php) answers
`Magento\InventoryCatalog\Model\GetStockIdForCurrentWebsite::execute()` from the entry
`FASTBOOT_STOCK_ID_<website code>`, under the `stock_id` switch.
[`Plugin/ForgetStockIds`](Plugin/ForgetStockIds.php) cleans the entries after a write of a
stock's sales channel links.
81 changes: 81 additions & 0 deletions src/FastBootInventory/Test/Unit/Plugin/StockIdFromCacheTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);

namespace GraphCommerce\FastBootInventory\Test\Unit\Plugin;

use GraphCommerce\FastBootCache\Model\Feature;
use GraphCommerce\FastBootCache\Model\Tag;
use GraphCommerce\FastBootInventory\Plugin\ForgetStockIds;
use GraphCommerce\FastBootInventory\Plugin\StockIdFromCache;
use Magento\Framework\App\CacheInterface;
use Magento\Framework\App\RequestInterface;
use Magento\InventoryCatalog\Model\GetStockIdForCurrentWebsite;
use Magento\InventorySales\Model\ResourceModel\ReplaceSalesChannelsDataForStock;
use Magento\Store\Api\Data\StoreInterface;
use Magento\Store\Api\Data\WebsiteInterface;
use Magento\Store\Model\StoreManagerInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

require_once __DIR__ . '/../_files/inventory-classes.php';

class StockIdFromCacheTest extends TestCase
{
private array $entries = [];
private CacheInterface&MockObject $cache;

private function cache(): CacheInterface&MockObject
{
$this->cache = $this->createMock(CacheInterface::class);
$this->cache->method('load')->willReturnCallback(fn(string $id) => $this->entries[$id] ?? false);
$this->cache->method('save')->willReturnCallback(function (string $data, string $id) {
$this->entries[$id] = $data;

return true;
});
$this->cache->method('clean')->willReturnCallback(function (array $tags) {
$this->entries = in_array(Tag::STOCK, $tags, true) ? [] : $this->entries;

return true;
});

return $this->cache;
}

private function plugin(): StockIdFromCache
{
$feature = $this->createStub(Feature::class);
$feature->method('on')->willReturn(true);

$store = $this->createStub(StoreInterface::class);
$store->method('getWebsiteId')->willReturn(1);
$website = $this->createStub(WebsiteInterface::class);
$website->method('getCode')->willReturn('base');
$storeManager = $this->createStub(StoreManagerInterface::class);
$storeManager->method('getStore')->willReturn($store);
$storeManager->method('getWebsite')->willReturn($website);

return new StockIdFromCache($this->cache(), $feature, $storeManager, $this->createStub(RequestInterface::class));
}

public function testTheStockIdIsAnEntryUntilTheSalesChannelsChange(): void
{
$plugin = $this->plugin();
$subject = $this->createStub(GetStockIdForCurrentWebsite::class);
$calls = 0;
$proceed = function () use (&$calls): int {
$calls++;

return 3;
};

$this->assertSame(3, $plugin->aroundExecute($subject, $proceed));
$this->assertSame(3, $plugin->aroundExecute($subject, $proceed));
$this->assertSame(1, $calls);
$this->assertSame('{"id":3}', $this->entries['FASTBOOT_STOCK_ID_base']);

(new ForgetStockIds($this->cache))->afterExecute($this->createStub(ReplaceSalesChannelsDataForStock::class), null);
$plugin->aroundExecute($subject, $proceed);
$this->assertSame(2, $calls);
}
}
28 changes: 28 additions & 0 deletions src/FastBootInventory/Test/Unit/_files/inventory-classes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php
/**
* The MSI classes the plugins act on, for a unit run without the inventory modules.
*/
declare(strict_types=1);

namespace Magento\InventoryCatalog\Model {
if (!class_exists(GetStockIdForCurrentWebsite::class)) {
class GetStockIdForCurrentWebsite
{
public function execute(): int
{
return 0;
}
}
}
}

namespace Magento\InventorySales\Model\ResourceModel {
if (!class_exists(ReplaceSalesChannelsDataForStock::class)) {
class ReplaceSalesChannelsDataForStock
{
public function execute(array $salesChannels, int $stockId): void
{
}
}
}
}
22 changes: 22 additions & 0 deletions src/FastBootInventory/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "graphcommerce/module-fast-boot-inventory",
"description": "The stock of a website from the cache instead of the sales channel selects per request",
"type": "magento2-module",
"license": "MIT",
"require": {
"php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
"graphcommerce/module-fast-boot-cache": "*",
"magento/framework": "^103.0.8",
"magento/module-inventory-catalog": "*",
"magento/module-inventory-sales": "*",
"magento/module-store": "^101.1.8"
},
"autoload": {
"files": [
"registration.php"
],
"psr-4": {
"GraphCommerce\\FastBootInventory\\": ""
}
}
}
27 changes: 27 additions & 0 deletions src/FastBootInventory/etc/di.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="GraphCommerce\FastBootCache\Plugin\OpcacheDefaultLayer">
<arguments>
<argument name="ids" xsi:type="array">
<item name="stock_ids" xsi:type="string">FASTBOOT_STOCK_ID_</item>
</argument>
<argument name="tags" xsi:type="array">
<item name="stock" xsi:type="const">GraphCommerce\FastBootCache\Model\Tag::STOCK</item>
</argument>
</arguments>
</type>

<!-- The stock id of the current website from the cache; a change of a stock's sales channels cleans it. -->
<type name="Magento\InventoryCatalog\Model\GetStockIdForCurrentWebsite">
<plugin name="graphcommerce_fastboot_stock_id" type="GraphCommerce\FastBootInventory\Plugin\StockIdFromCache"/>
</type>
<type name="Magento\InventorySales\Model\ResourceModel\ReplaceSalesChannelsDataForStock">
<plugin name="graphcommerce_fastboot_forget_stock_ids" type="GraphCommerce\FastBootInventory\Plugin\ForgetStockIds"/>
</type>
<type name="Magento\InventorySales\Model\ResourceModel\DeleteSalesChannelToStockLink">
<plugin name="graphcommerce_fastboot_forget_stock_ids" type="GraphCommerce\FastBootInventory\Plugin\ForgetStockIds"/>
</type>
<type name="Magento\InventorySales\Model\ResourceModel\UpdateSalesChannelWebsiteCode">
<plugin name="graphcommerce_fastboot_forget_stock_ids" type="GraphCommerce\FastBootInventory\Plugin\ForgetStockIds"/>
</type>
</config>
10 changes: 10 additions & 0 deletions src/FastBootInventory/etc/module.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="GraphCommerce_FastBootInventory">
<sequence>
<module name="GraphCommerce_FastBootCache"/>
<module name="Magento_InventoryCatalog"/>
<module name="Magento_InventorySales"/>
</sequence>
</module>
</config>
6 changes: 6 additions & 0 deletions src/FastBootInventory/registration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?php
declare(strict_types=1);

use Magento\Framework\Component\ComponentRegistrar;

ComponentRegistrar::register(ComponentRegistrar::MODULE, 'GraphCommerce_FastBootInventory', __DIR__);