| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161 |
- <?php
- namespace Webkul\BagistoApi\State;
- use ApiPlatform\Laravel\Eloquent\Paginator;
- use ApiPlatform\Metadata\Operation;
- use ApiPlatform\State\Pagination\Pagination;
- use ApiPlatform\State\ProviderInterface;
- use Illuminate\Pagination\LengthAwarePaginator;
- use Webkul\BagistoApi\Models\ProductQuestion;
- use Webkul\BagistoApi\Models\ProductQuestionAnswer;
- class ProductQuestionProvider implements ProviderInterface
- {
- public function __construct(
- private readonly Pagination $pagination
- ) {}
- public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
- {
- $args = $context['args'] ?? [];
- $request = request();
- $query = ProductQuestion::query();
- // Filter by product_id from URI variables (nested route) or query param
- $productId = $uriVariables['productId'] ?? $args['product_id'] ?? $request->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,
- ]);
- });
- }
- }
|