query('product_id'); if ($productId) { $query->where('product_id', (int) $productId); } // Storefront shows only approved questions by default $status = $args['status'] ?? $request->query('status', 'approved'); $query->where('status', $status); $query->with(['customer']); $query->orderByDesc('created_at'); // --------------------------------------------------------------- // Embedded answers options // with_answers=true (default) → embed top N answers per question // answers_per_question=5 → how many answers to embed (max 20) // answer_status=approved → which answer status to embed // --------------------------------------------------------------- $withAnswers = filter_var( $args['with_answers'] ?? $request->query('with_answers', 'true'), FILTER_VALIDATE_BOOLEAN ); $answersPerQuestion = min(20, max(1, (int) ( $args['answers_per_question'] ?? $request->query('answers_per_question', 5) ))); $answerStatus = $args['answer_status'] ?? $request->query('answer_status', 'approved'); // REST pagination $page = max(1, (int) ($request->query('page', 1))); $perPage = min(50, max(1, (int) ($request->query('per_page', $request->query('limit', 30))))); // GraphQL cursor pagination $first = isset($args['first']) ? (int) $args['first'] : null; $last = isset($args['last']) ? (int) $args['last'] : null; $after = $args['after'] ?? null; $before = $args['before'] ?? null; if ($first || $last || $after || $before) { $perPage = $first ?? $last ?? 30; $offset = 0; if ($after) { $decoded = base64_decode($after, true); $offset = ctype_digit((string) $decoded) ? ((int) $decoded + 1) : 0; } if ($before) { $decoded = base64_decode($before, true); $cursor = ctype_digit((string) $decoded) ? (int) $decoded : 0; $offset = max(0, $cursor - $perPage); } $total = (clone $query)->count(); $items = $query->offset($offset)->limit($perPage)->get(); $currentPage = $total > 0 ? (int) floor($offset / $perPage) + 1 : 1; } else { $total = (clone $query)->count(); $offset = ($page - 1) * $perPage; $items = $query->offset($offset)->limit($perPage)->get(); $currentPage = $page; } // Attach answers preview to each question in a single batch query (no N+1) if ($withAnswers && $items->isNotEmpty()) { $this->attachAnswersPreview($items, $answersPerQuestion, $answerStatus); } else { $items->each(function ($question) { $question->setAttribute('answers_preview', []); $question->setAttribute('answers_meta', [ 'total' => $question->answers_count, 'has_more' => false, 'page_size' => 0, ]); }); } return new Paginator( new LengthAwarePaginator( $items, $total, $perPage, $currentPage, ['path' => request()->url()] ) ); } /** * Load top N answers for all questions in one query, group by question_id, * and attach as `answers_preview` + `answers_meta` attributes. * * Sort order: pinned first → useful_count desc → newest first. * This avoids N+1 — only one extra DB query for the whole page. */ private function attachAnswersPreview( \Illuminate\Support\Collection $questions, int $limit, string $status ): void { $questionIds = $questions->pluck('id')->toArray(); // Fetch enough rows to let us take $limit per question after grouping. // We over-fetch by $limit × question count so we can slice in PHP. $answers = ProductQuestionAnswer::whereIn('question_id', $questionIds) ->where('status', $status) ->orderByDesc('is_pinned') ->orderByDesc('useful_count') ->orderByDesc('created_at') ->get(['id', 'question_id', 'customer_id', 'customer_name', 'answer', 'status', 'is_pinned', 'useful_count', 'created_at', 'updated_at']); // Total counts per question (separate lightweight query) $totalCounts = ProductQuestionAnswer::whereIn('question_id', $questionIds) ->where('status', $status) ->groupBy('question_id') ->selectRaw('question_id, COUNT(*) as cnt') ->pluck('cnt', 'question_id'); // Group and slice to top N per question $grouped = $answers->groupBy('question_id') ->map(fn ($group) => $group->take($limit)->values()); $questions->each(function ($question) use ($grouped, $totalCounts, $limit) { $preview = $grouped->get($question->id, collect()); $total = (int) ($totalCounts->get($question->id, 0)); $question->setAttribute('answers_preview', $preview->toArray()); $question->setAttribute('answers_meta', [ 'total' => $total, 'has_more' => $total > $limit, 'page_size' => $limit, ]); }); } }