diff --git a/CLAUDE.md b/CLAUDE.md index 5c59252..4a55bcf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. diff --git a/src/CatalogStorefrontConfigurableProductFrontend/Plugin/Listing/ParentIdsFromDocuments.php b/src/CatalogStorefrontConfigurableProductFrontend/Plugin/Listing/ParentIdsFromDocuments.php new file mode 100644 index 0000000..1d68645 --- /dev/null +++ b/src/CatalogStorefrontConfigurableProductFrontend/Plugin/Listing/ParentIdsFromDocuments.php @@ -0,0 +1,61 @@ +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); + } +} diff --git a/src/CatalogStorefrontConfigurableProductFrontend/README.md b/src/CatalogStorefrontConfigurableProductFrontend/README.md index 0f09a01..dd3f467 100644 --- a/src/CatalogStorefrontConfigurableProductFrontend/README.md +++ b/src/CatalogStorefrontConfigurableProductFrontend/README.md @@ -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. diff --git a/src/CatalogStorefrontConfigurableProductFrontend/Test/Unit/Plugin/Listing/ParentIdsFromDocumentsTest.php b/src/CatalogStorefrontConfigurableProductFrontend/Test/Unit/Plugin/Listing/ParentIdsFromDocumentsTest.php new file mode 100644 index 0000000..7a2e9da --- /dev/null +++ b/src/CatalogStorefrontConfigurableProductFrontend/Test/Unit/Plugin/Listing/ParentIdsFromDocumentsTest.php @@ -0,0 +1,124 @@ +|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); + } +} diff --git a/src/CatalogStorefrontConfigurableProductFrontend/etc/di.xml b/src/CatalogStorefrontConfigurableProductFrontend/etc/di.xml index 37068f9..41913a1 100644 --- a/src/CatalogStorefrontConfigurableProductFrontend/etc/di.xml +++ b/src/CatalogStorefrontConfigurableProductFrontend/etc/di.xml @@ -1,11 +1,13 @@ - + +