diff --git a/CLAUDE.md b/CLAUDE.md index a2c067c..499a7c3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -499,7 +499,10 @@ image URLs come from the feed row (`Model/DataExporter/Provider/ImageUrls`). names, each remaining slice in the shape its reader takes, so no value is stored twice and no reader parses at request time (`customAttributes` the raw value per attribute code, an image type and a gallery entry the media file, a - URL rewrite the request path, `categoryData` as `categoryIds`), + URL rewrite the request path, `categoryData` as `categoryIds`, the category product index with + the anchor ancestors; `categoryAssignmentIds` (`Model/DataExporter/Provider/CategoryAssignments`) + holds the rows of `catalog_category_product`, which the model takes as `category_ids` because core's + `getCategoryIds()` reads them), prices = the feed rows under `prices` with their customer `group` id (the feed names a group by the hash of its id; the writer maps it back, rows of an unknown group are dropped; the feed's all-groups row comes diff --git a/src/CatalogStorefront/Model/DataExporter/Provider/CategoryAssignments.php b/src/CatalogStorefront/Model/DataExporter/Provider/CategoryAssignments.php new file mode 100644 index 0000000..e521c9c --- /dev/null +++ b/src/CatalogStorefront/Model/DataExporter/Provider/CategoryAssignments.php @@ -0,0 +1,50 @@ + (int)$value['productId'], $values)); + if (!$ids) { + return []; + } + $connection = $this->resourceConnection->getConnection(); + $categories = []; + $select = $connection->select() + ->from($this->resourceConnection->getTableName('catalog_category_product'), ['product_id', 'category_id']) + ->where('product_id IN (?)', $ids) + ->order('category_id'); + foreach ($connection->fetchAll($select) as $row) { + $categories[(int)$row['product_id']][] = (int)$row['category_id']; + } + + $output = []; + foreach ($values as $value) { + foreach ($categories[(int)$value['productId']] ?? [] as $categoryId) { + $output[$value['storeViewCode'] . '_' . $value['productId'] . '_' . $categoryId] = [ + 'productId' => $value['productId'], + 'storeViewCode' => $value['storeViewCode'], + 'categoryAssignmentIds' => $categoryId, + ]; + } + } + + return $output; + } +} diff --git a/src/CatalogStorefront/Model/Read/ProductModelBuilder.php b/src/CatalogStorefront/Model/Read/ProductModelBuilder.php index 0462628..446cb65 100644 --- a/src/CatalogStorefront/Model/Read/ProductModelBuilder.php +++ b/src/CatalogStorefront/Model/Read/ProductModelBuilder.php @@ -98,8 +98,11 @@ public function build(array $document, int $storeId): ?Product $data['price'] = $fallbackRow['regular']; } - if (!empty($document['categoryIds'])) { - $data['category_ids'] = array_map('strval', $document['categoryIds']); + // Core's getCategoryIds() reads the assignments. `categoryIds` holds the category product + // index, which adds the anchor ancestors: the current category of a page then applies to + // the model where it does not apply to core's, and the product URL takes the category path. + if (!empty($document['categoryAssignmentIds'])) { + $data['category_ids'] = array_map('strval', $document['categoryAssignmentIds']); } foreach ((array)($document['customAttributes'] ?? []) as $code => $value) { diff --git a/src/CatalogStorefront/Test/Unit/Model/DataExporter/Provider/CategoryAssignmentsTest.php b/src/CatalogStorefront/Test/Unit/Model/DataExporter/Provider/CategoryAssignmentsTest.php new file mode 100644 index 0000000..9e291a2 --- /dev/null +++ b/src/CatalogStorefront/Test/Unit/Model/DataExporter/Provider/CategoryAssignmentsTest.php @@ -0,0 +1,62 @@ +provider([ + ['product_id' => '1812', 'category_id' => '2'], + ['product_id' => '1812', 'category_id' => '30'], + ['product_id' => '6', 'category_id' => '4'], + ]); + + $output = $provider->get([ + ['productId' => '1812', 'storeViewCode' => 'default'], + ['productId' => '6', 'storeViewCode' => 'default'], + ['productId' => '7', 'storeViewCode' => 'default'], + ['productId' => '1812', 'storeViewCode' => 'second'], + ]); + + self::assertSame([ + 'default_1812_2' => ['productId' => '1812', 'storeViewCode' => 'default', 'categoryAssignmentIds' => 2], + 'default_1812_30' => ['productId' => '1812', 'storeViewCode' => 'default', 'categoryAssignmentIds' => 30], + 'default_6_4' => ['productId' => '6', 'storeViewCode' => 'default', 'categoryAssignmentIds' => 4], + 'second_1812_2' => ['productId' => '1812', 'storeViewCode' => 'second', 'categoryAssignmentIds' => 2], + 'second_1812_30' => ['productId' => '1812', 'storeViewCode' => 'second', 'categoryAssignmentIds' => 30], + ], $output); + } + + public function testNoProductsMeansNoQuery(): void + { + $resource = $this->createMock(ResourceConnection::class); + $resource->expects(self::never())->method('getConnection'); + + self::assertSame([], (new CategoryAssignments($resource))->get([])); + } + + private function provider(array $rows): CategoryAssignments + { + $select = $this->createMock(Select::class); + $select->method('from')->willReturnSelf(); + $select->method('where')->willReturnSelf(); + $select->method('order')->willReturnSelf(); + $connection = $this->createMock(AdapterInterface::class); + $connection->method('select')->willReturn($select); + $connection->method('fetchAll')->willReturn($rows); + $resource = $this->createMock(ResourceConnection::class); + $resource->method('getConnection')->willReturn($connection); + $resource->method('getTableName')->willReturnArgument(0); + + return new CategoryAssignments($resource); + } +} diff --git a/src/CatalogStorefront/Test/Unit/Model/Read/ProductModelBuilderTest.php b/src/CatalogStorefront/Test/Unit/Model/Read/ProductModelBuilderTest.php index 4637d10..7fd4a8c 100644 --- a/src/CatalogStorefront/Test/Unit/Model/Read/ProductModelBuilderTest.php +++ b/src/CatalogStorefront/Test/Unit/Model/Read/ProductModelBuilderTest.php @@ -118,11 +118,12 @@ public function testAFixedBundleIsBuiltAsABundle(): void $this->assertSame('1', $this->data['price_type']); } - public function testCategoryIdsAreTheDocumentsCategoryIdsAsStrings(): void + public function testCategoryIdsAreTheAssignmentsAsStringsNotTheIndex(): void { - $this->build(['categoryIds' => [24, 25]]); + // The index adds the anchor ancestors 24 and 25 and leaves out the root 2. + $this->build(['categoryIds' => [24, 25, 30, 34], 'categoryAssignmentIds' => [2, 30, 34]]); - $this->assertSame(['24', '25'], $this->data['category_ids']); + $this->assertSame(['2', '30', '34'], $this->data['category_ids']); } public function testAnAttributeValueComesFromTheCustomAttributeMap(): void diff --git a/src/CatalogStorefront/etc/et_schema.xml b/src/CatalogStorefront/etc/et_schema.xml index 0fffc89..1e29044 100644 --- a/src/CatalogStorefront/etc/et_schema.xml +++ b/src/CatalogStorefront/etc/et_schema.xml @@ -29,6 +29,10 @@ + + + +