| @@ -94,9 +94,9 @@ | ||
| 94 | 94 | |
| 95 | 95 | |
| 96 | 96 | $query = static::getQuery() |
| 97 | 97 | ->select(Arr::get($params, 'select', '*')) |
| 98 | - ->with(Arr::get($params, 'with', [])); | |
| 98 | + ->with(static::expandAppendRelations(Arr::get($params, 'with', []))); | |
| 99 | 99 | |
| 100 | 100 | $query = apply_filters('fluent_cart/shop_query', $query, $params); |
| 101 | 101 | |
| 102 | 102 | $query = $query->when(!Arr::get($params, 'selected_status'), function ($query) use ($params) { |
| @@ -261,8 +261,10 @@ | ||
| 261 | 261 | } else { |
| 262 | 262 | $products = $query->simplePaginate(Arr::get($params, 'per_page', 10), ['*'], 'current_page', Arr::get($params, 'page')); |
| 263 | 263 | } |
| 264 | 264 | |
| 265 | + static::primePostCaches($products); | |
| 266 | + | |
| 265 | 267 | return [ |
| 266 | 268 | 'products' => $products, |
| 267 | 269 | 'total' => $totalCount |
| 268 | 270 | ]; |
| @@ -268,8 +270,66 @@ | ||
| 268 | 270 | ]; |
| 269 | 271 | } |
| 270 | 272 | |
| 271 | 273 | /** |
| 274 | + * Expand the caller's `with` list so the relations that the models' default | |
| 275 | + * appends read are eager loaded rather than fetched one row at a time. | |
| 276 | + * | |
| 277 | + * `ProductVariation::$appends` renders `thumbnail` from its `media` relation and | |
| 278 | + * `ProductDetail::$appends` renders `featured_media` from `galleryImage`, so every | |
| 279 | + * serialized row issued its own query — one per variant plus one per product, on a | |
| 280 | + * public list endpoint. Only relations the caller already asked for are expanded, | |
| 281 | + * so the response shape is byte-identical; callers that never load `variants` or | |
| 282 | + * `detail` are untouched. | |
| 283 | + * | |
| 284 | + * @param array|string $with Relations the caller requested. | |
| 285 | + * @return array | |
| 286 | + */ | |
| 287 | + protected static function expandAppendRelations($with): array | |
| 288 | + { | |
| 289 | + $with = Arr::wrap($with); | |
| 290 | + | |
| 291 | + $appendRelations = [ | |
| 292 | + 'variants' => 'variants.media', | |
| 293 | + 'detail' => 'detail.galleryImage', | |
| 294 | + ]; | |
| 295 | + | |
| 296 | + foreach ($appendRelations as $relation => $nestedRelation) { | |
| 297 | + if (in_array($relation, $with, true) && !in_array($nestedRelation, $with, true)) { | |
| 298 | + $with[] = $nestedRelation; | |
| 299 | + } | |
| 300 | + } | |
| 301 | + | |
| 302 | + return $with; | |
| 303 | + } | |
| 304 | + | |
| 305 | + /** | |
| 306 | + * Prime the WordPress post cache for the products on this page. | |
| 307 | + * | |
| 308 | + * These rows come from the ORM, not WP_Query, so nothing populates the `posts` | |
| 309 | + * cache. Every `get_permalink()` behind the `view_url` append — and every core | |
| 310 | + * template helper a storefront card calls — then fell through to `get_post()`, | |
| 311 | + * one SELECT per product. This replaces them with a single `IN (...)` read. | |
| 312 | + * Terms and meta are deliberately not primed: no product-list path reads them | |
| 313 | + * here, and priming them would cost two more queries. | |
| 314 | + * | |
| 315 | + * @param mixed $products Paginator returned by the list query. | |
| 316 | + * @return void | |
| 317 | + */ | |
| 318 | + protected static function primePostCaches($products) | |
| 319 | + { | |
| 320 | + if (!$products || !method_exists($products, 'getCollection')) { | |
| 321 | + return; | |
| 322 | + } | |
| 323 | + | |
| 324 | + $postIds = $products->getCollection()->pluck('ID')->filter()->all(); | |
| 325 | + | |
| 326 | + if ($postIds) { | |
| 327 | + _prime_post_caches($postIds, false, false); | |
| 328 | + } | |
| 329 | + } | |
| 330 | + | |
| 331 | + /** | |
| 272 | 332 | * Find product by its ID. |
| 273 | 333 | * |
| 274 | 334 | * @param int $productId The ID of the post. |
| 275 | 335 | * @param array $data Additional data for finding product (optional). |
| @@ -341,8 +401,21 @@ | ||
| 341 | 401 | } else { |
| 342 | 402 | $args['orderby'] = $orderField; |
| 343 | 403 | $args['order'] = $orderDir; |
| 344 | 404 | } |
| 405 | + | |
| 406 | + // Filters the query arguments used to fetch related products. | |
| 407 | + // Developers can customize the query as needed, such as excluding products | |
| 408 | + // or modifying the query parameters. | |
| 409 | + $args = apply_filters( | |
| 410 | + 'fluent_cart/related_products/query_args', | |
| 411 | + $args, | |
| 412 | + [ | |
| 413 | + 'product_id' => $id, | |
| 414 | + 'post' => $post, | |
| 415 | + 'config' => $config, | |
| 416 | + ] | |
| 417 | + ); | |
| 345 | 418 | |
| 346 | 419 | $query = new \WP_Query($args); |
| 347 | 420 | |
| 348 | 421 | // Remove the price filter if applied |