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
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,10 @@ passes.
swatch block's cache key calls before the block cache, so it is paid on
every render; the lowest price options provider picks the cheapest child by
final and by regular price from `priceIndex` (per-variant discounts make
those different children) and wraps core's provider for the rest. A child's
those different children) and wraps core's provider for the rest;
`getParentIdsByChild()` answers from the `parentIds` of the page's card and
variant documents, which core's identities plugin asks for every simple
product a card tags, a query each on every render. A child's
`entity_id` and a super attribute's `position` are strings, since
`getJsonConfig()` puts them into the rendered JSON as they are. Without
the `configurableOptions` field the plugin hands over to core with nothing logged.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);

namespace GraphCommerce\CatalogStorefrontConfigurableProductFrontend\Plugin\Listing;

use GraphCommerce\CatalogStorefrontConfigurableProductFrontend\Model\Read\VariantDocuments;
use GraphCommerce\CatalogStorefrontProductFrontend\Model\Read\ListingDocuments;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
use Magento\Store\Model\StoreManagerInterface;

/**
* Answers the configurable parents of a card product, or of a variant of a card, from its
* document. Core's identities plugin asks them for every simple product whose identities a card
* collects, a query per product on every render, also when the card's block cache hits. A product
* whose document the page did not read goes to core.
*/
class ParentIdsFromDocuments
{
public function __construct(
private readonly ListingDocuments $page,
private readonly VariantDocuments $variants,
private readonly StoreManagerInterface $storeManager,
) {
}

/**
* @param Configurable $subject
* @param \Closure $proceed
* @param int|string|int[]|string[] $childId
* @return string[]
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function aroundGetParentIdsByChild(Configurable $subject, \Closure $proceed, $childId)
{
$store = $this->storeManager->getStore();
$cards = $this->page->documents((string)$store->getCode());
if ($cards === []) {
return $proceed($childId);
}

$ids = array_map('intval', (array)$childId);
$documents = array_intersect_key($cards, array_flip($ids));
$rest = array_values(array_diff($ids, array_keys($documents)));
if ($rest !== []) {
$variants = $this->variants->documents($store, $rest);
if ($variants === null || count($variants) !== count($rest)) {
return $proceed($childId);
}
$documents += $variants;
}

$parentIds = [];
foreach ($documents as $document) {
foreach ((array)($document['parentIds'] ?? []) as $parentId) {
$parentIds[(string)$parentId] = (string)$parentId;
}
}

return array_values($parentIds);
}
}
3 changes: 3 additions & 0 deletions src/CatalogStorefrontConfigurableProductFrontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@ GraphCommerce_CatalogStorefrontProductFrontend.
the page's child price ranges, so the card prices without its variant documents. A product
detail page, a store with fixed product taxes and a product without a range pick the
cheapest child by final price and by regular price from `priceIndex`.
- `getParentIdsByChild()` answers from the `parentIds` of a card's document or of a card
variant's document. Core's identities plugin asks it for every simple product whose cache
tags a card collects, a query per product on every render, also on a block cache hit.

Each seam hands a product without a document to core.
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);

namespace GraphCommerce\CatalogStorefrontConfigurableProductFrontend\Test\Unit\Plugin\Listing;

use GraphCommerce\CatalogStorefrontConfigurableProductFrontend\Model\Read\VariantDocuments;
use GraphCommerce\CatalogStorefrontConfigurableProductFrontend\Plugin\Listing\ParentIdsFromDocuments;
use GraphCommerce\CatalogStorefrontProductFrontend\Model\Read\ListingDocuments;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
use Magento\Store\Api\Data\StoreInterface;
use Magento\Store\Model\StoreManagerInterface;
use PHPUnit\Framework\TestCase;

class ParentIdsFromDocumentsTest extends TestCase
{
private int $coreCalls = 0;

/** @var int[][] the child ids each variant read asked for */
private array $variantReads = [];

/**
* @param array<int, array>|null $variants what the variant read answers
*/
private function plugin(ListingDocuments $page, ?array $variants = null): ParentIdsFromDocuments
{
$store = $this->createMock(StoreInterface::class);
$store->method('getCode')->willReturn('default');
$storeManager = $this->createMock(StoreManagerInterface::class);
$storeManager->method('getStore')->willReturn($store);

$variantDocuments = $this->createMock(VariantDocuments::class);
$variantDocuments->method('documents')->willReturnCallback(
function ($store, array $childIds) use ($variants): ?array {
$this->variantReads[] = $childIds;

return $variants === null ? null : array_intersect_key($variants, array_flip($childIds));
}
);

return new ParentIdsFromDocuments($page, $variantDocuments, $storeManager);
}

private function page(array $documents, string $storeViewCode = 'default'): ListingDocuments
{
$page = new ListingDocuments();
$page->add($storeViewCode, $documents);

return $page;
}

private function call(ParentIdsFromDocuments $plugin, $childId): array
{
return $plugin->aroundGetParentIdsByChild(
$this->createMock(Configurable::class),
function () {
$this->coreCalls++;

return ['core'];
},
$childId
);
}

public function testCardAnswersFromItsDocument(): void
{
$plugin = $this->plugin($this->page([10 => ['parentIds' => [7, 8]]]));

$this->assertSame(['7', '8'], $this->call($plugin, '10'));
$this->assertSame([], $this->variantReads);
$this->assertSame(0, $this->coreCalls);
}

public function testCardWithoutParentsAnswersEmpty(): void
{
$plugin = $this->plugin($this->page([10 => ['sku' => 'simple']]));

$this->assertSame([], $this->call($plugin, 10));
$this->assertSame(0, $this->coreCalls);
}

public function testVariantOfACardAnswersFromTheVariantDocuments(): void
{
$plugin = $this->plugin($this->page([7 => ['variantIds' => [21]]]), [21 => ['parentIds' => [7]]]);

$this->assertSame(['7'], $this->call($plugin, 21));
$this->assertSame([[21]], $this->variantReads);
$this->assertSame(0, $this->coreCalls);
}

public function testSeveralProductsMergeTheirParentsOnce(): void
{
$plugin = $this->plugin(
$this->page([10 => ['parentIds' => [7]]]),
[21 => ['parentIds' => [7, 9]]]
);

$this->assertSame(['7', '9'], $this->call($plugin, [10, 21]));
$this->assertSame([[21]], $this->variantReads);
}

public function testProductWithoutADocumentGoesToCore(): void
{
$plugin = $this->plugin($this->page([10 => ['parentIds' => [7]]]), [21 => ['parentIds' => [7]]]);

$this->assertSame(['core'], $this->call($plugin, [10, 22]));
$this->assertSame(1, $this->coreCalls);
}

public function testVariantReadThatCoversNothingGoesToCore(): void
{
$plugin = $this->plugin($this->page([7 => ['variantIds' => [21]]]));

$this->assertSame(['core'], $this->call($plugin, 21));
$this->assertSame(1, $this->coreCalls);
}

public function testRequestWithoutListingDocumentsGoesToCore(): void
{
$this->assertSame(['core'], $this->call($this->plugin(new ListingDocuments()), 10));
$this->assertSame(['core'], $this->call($this->plugin($this->page([10 => []], 'other')), 10));
$this->assertSame(2, $this->coreCalls);
$this->assertSame([], $this->variantReads);
}
}
4 changes: 3 additions & 1 deletion src/CatalogStorefrontConfigurableProductFrontend/etc/di.xml
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<?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">
<!-- Both act only on a product that carries a document, so a core-served page is untouched. -->
<!-- They act only on a product that carries a document, so a core-served page is untouched. -->
<type name="Magento\ConfigurableProduct\Model\Product\Type\Configurable">
<plugin name="graphcommerce_catalog_storefront_used_products"
type="GraphCommerce\CatalogStorefrontConfigurableProductFrontend\Plugin\Listing\UsedProductsFromDocuments"/>
<plugin name="graphcommerce_catalog_storefront_configurable_attributes"
type="GraphCommerce\CatalogStorefrontConfigurableProductFrontend\Plugin\Listing\ConfigurableAttributesFromDocument"/>
<plugin name="graphcommerce_catalog_storefront_parent_ids"
type="GraphCommerce\CatalogStorefrontConfigurableProductFrontend\Plugin\Listing\ParentIdsFromDocuments"/>
</type>

<!-- The swatch renderer of a document product renders without the block cache. -->
Expand Down