From 15a7b8b639e0b306161a6b1be19f91c1bd0e4e33 Mon Sep 17 00:00:00 2001 From: Sam James Date: Tue, 14 Jul 2026 09:43:05 +0100 Subject: [PATCH] fix: apply category preload after toolbar sort order, not before MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The preload plugin ran as an afterExecute on the category controller, which forces the product collection to load (via getLoadedProductCollection) before the Toolbar block has applied the current sort order in _beforeToHtml. Once a collection is loaded, later addOrder/setOrder calls from Toolbar::setCollection have no effect, so sort order broke whenever category preload was enabled. Move the hook to an afterSetCollection plugin on Magento\Catalog\Block\Product\ProductList\Toolbar instead — order and paging are already applied to the collection by the time this plugin runs, so iterating it here to grab preload images no longer races the toolbar's sort. --- .../Product/ProductList/SetCollection.php} | 21 ++- .../Product/ProductList/SetCollectionTest.php | 147 ++++++++++++++++++ etc/di.xml | 4 +- 3 files changed, 158 insertions(+), 14 deletions(-) rename Plugin/Catalog/{Category/View.php => Block/Product/ProductList/SetCollection.php} (82%) create mode 100644 Test/Unit/Plugin/Catalog/Block/Product/ProductList/SetCollectionTest.php diff --git a/Plugin/Catalog/Category/View.php b/Plugin/Catalog/Block/Product/ProductList/SetCollection.php similarity index 82% rename from Plugin/Catalog/Category/View.php rename to Plugin/Catalog/Block/Product/ProductList/SetCollection.php index 5e66090..6974acd 100644 --- a/Plugin/Catalog/Category/View.php +++ b/Plugin/Catalog/Block/Product/ProductList/SetCollection.php @@ -2,20 +2,20 @@ declare(strict_types=1); -namespace SamJUK\FetchPriority\Plugin\Catalog\Category; +namespace SamJUK\FetchPriority\Plugin\Catalog\Block\Product\ProductList; +use Magento\Catalog\Block\Product\ProductList\Toolbar; use Magento\Catalog\Model\View\Asset\ImageFactory as AssetImageFactory; use Magento\Catalog\Model\Product\Image\ParamsBuilder; use Magento\Catalog\Model\View\Asset\PlaceholderFactory; use Magento\Framework\View\ConfigInterface; use Magento\Catalog\Helper\Image as ImageHelper; -class View +class SetCollection { public function __construct( private readonly \SamJUK\FetchPriority\Model\LinkStore $linkStore, private readonly \SamJUK\FetchPriority\Model\Links\PreloadFactory $preloadFactory, - private readonly \Magento\Catalog\Block\Product\ListProduct $listProductBlock, private readonly ConfigInterface $presentationConfig, private readonly ParamsBuilder $imageParamsBuilder, private readonly PlaceholderFactory $viewAssetPlaceholderFactory, @@ -23,21 +23,20 @@ public function __construct( private readonly \SamJUK\FetchPriority\Model\Config $config ) { } - public function afterExecute($subject, $result) + public function afterSetCollection(Toolbar $subject, $result, $collection) { if (!$this->config->isEnabled() || !$this->config->isCategoryProductPreloadEnabled()) { return $result; } - $this->preloadInitialProductImages(); + $this->preloadInitialProductImages($collection, $subject->getCurrentMode()); return $result; } - public function preloadInitialProductImages() + private function preloadInitialProductImages($collection, string $mode): void { $i = 0; - $imageType = $this->getImageType(); - $collection = $this->listProductBlock->getLoadedProductCollection(); + $imageType = $this->getImageType($mode); foreach ($collection as $product) { if (++$i > 4) { return; @@ -76,11 +75,9 @@ private function getProductImage($product, $imageType) return $imageAsset->getUrl(); } - private function getImageType() + private function getImageType(string $mode) { - return $this->listProductBlock->getMode() === 'grid' - ? 'category_page_grid' - : 'category_page_list'; + return $mode === 'grid' ? 'category_page_grid' : 'category_page_list'; } private function preload(string $imageUrl) diff --git a/Test/Unit/Plugin/Catalog/Block/Product/ProductList/SetCollectionTest.php b/Test/Unit/Plugin/Catalog/Block/Product/ProductList/SetCollectionTest.php new file mode 100644 index 0000000..78395b5 --- /dev/null +++ b/Test/Unit/Plugin/Catalog/Block/Product/ProductList/SetCollectionTest.php @@ -0,0 +1,147 @@ +preloadFactoryMock = $this->getMockBuilder(PreloadFactory::class) + ->disableOriginalConstructor() + ->onlyMethods(['create']) + ->getMock(); + + $this->linkStoreMock = $this->createMock(LinkStore::class); + $this->presentationConfigMock = $this->createMock(ConfigInterface::class); + $this->imageParamsBuilderMock = $this->createMock(ParamsBuilder::class); + $this->viewAssetPlaceholderFactoryMock = $this->createMock(PlaceholderFactory::class); + $this->viewAssetImageFactoryMock = $this->createMock(AssetImageFactory::class); + $this->configMock = $this->createMock(Config::class); + $this->toolbarMock = $this->createMock(Toolbar::class); + + $viewConfigMock = $this->createMock(ViewConfig::class); + $viewConfigMock->method('getMediaAttributes')->willReturn(['image_type' => 'small_image']); + $this->presentationConfigMock->method('getViewConfig')->willReturn($viewConfigMock); + $this->imageParamsBuilderMock->method('build')->willReturn(['image_type' => 'small_image']); + + $this->subject = new SetCollection( + $this->linkStoreMock, + $this->preloadFactoryMock, + $this->presentationConfigMock, + $this->imageParamsBuilderMock, + $this->viewAssetPlaceholderFactoryMock, + $this->viewAssetImageFactoryMock, + $this->configMock + ); + } + + public function testDoesNothingWhenModuleDisabled(): void + { + $this->configMock->method('isEnabled')->willReturn(false); + $this->configMock->method('isCategoryProductPreloadEnabled')->willReturn(true); + + $this->linkStoreMock->expects($this->never())->method('add'); + + $this->subject->afterSetCollection($this->toolbarMock, $this->toolbarMock, []); + } + + public function testDoesNothingWhenCategoryPreloadDisabled(): void + { + $this->configMock->method('isEnabled')->willReturn(true); + $this->configMock->method('isCategoryProductPreloadEnabled')->willReturn(false); + + $this->linkStoreMock->expects($this->never())->method('add'); + + $this->subject->afterSetCollection($this->toolbarMock, $this->toolbarMock, []); + } + + public function testPreloadsProductsFromAlreadySortedCollection(): void + { + $this->configMock->method('isEnabled')->willReturn(true); + $this->configMock->method('isCategoryProductPreloadEnabled')->willReturn(true); + $this->toolbarMock->method('getCurrentMode')->willReturn('grid'); + + $productMock = $this->createMock(Product::class); + $productMock->method('getData')->with('small_image')->willReturn('product-image.jpg'); + + $imageAssetMock = $this->createMock(ViewAssetImage::class); + $imageAssetMock->method('getUrl')->willReturn('https://example.com/media/product-image.jpg'); + $this->viewAssetImageFactoryMock->method('create')->willReturn($imageAssetMock); + + $preloadMock = $this->createMock(Preload::class); + $this->preloadFactoryMock->expects($this->once()) + ->method('create') + ->with($this->callback(function ($args) { + return $args['href'] === 'https://example.com/media/product-image.jpg'; + })) + ->willReturn($preloadMock); + + $this->linkStoreMock->expects($this->once())->method('add')->with($preloadMock); + + // The collection is passed as the setCollection() argument, which by the time + // this plugin runs (afterSetCollection) already has sort order/pagination applied + // by the original method — iterating it here does not race the toolbar's ordering. + $result = $this->subject->afterSetCollection($this->toolbarMock, $this->toolbarMock, [$productMock]); + + $this->assertSame($this->toolbarMock, $result); + } + + public function testStopsPreloadingAfterFourProducts(): void + { + $this->configMock->method('isEnabled')->willReturn(true); + $this->configMock->method('isCategoryProductPreloadEnabled')->willReturn(true); + $this->toolbarMock->method('getCurrentMode')->willReturn('list'); + + $products = []; + for ($i = 0; $i < 6; $i++) { + $productMock = $this->createMock(Product::class); + $productMock->method('getData')->with('small_image')->willReturn("product-{$i}.jpg"); + $products[] = $productMock; + } + + $imageAssetMock = $this->createMock(ViewAssetImage::class); + $imageAssetMock->method('getUrl')->willReturn('https://example.com/media/product.jpg'); + $this->viewAssetImageFactoryMock->method('create')->willReturn($imageAssetMock); + + $preloadMock = $this->createMock(Preload::class); + $this->preloadFactoryMock->method('create')->willReturn($preloadMock); + + $this->linkStoreMock->expects($this->exactly(4))->method('add'); + + $this->subject->afterSetCollection($this->toolbarMock, $this->toolbarMock, $products); + } +} diff --git a/etc/di.xml b/etc/di.xml index e114b2e..8e755d0 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -1,6 +1,6 @@ - - + +