ProductQuestionProvider.php 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. <?php
  2. namespace Webkul\BagistoApi\State;
  3. use ApiPlatform\Laravel\Eloquent\Paginator;
  4. use ApiPlatform\Metadata\Operation;
  5. use ApiPlatform\State\Pagination\Pagination;
  6. use ApiPlatform\State\ProviderInterface;
  7. use Illuminate\Pagination\LengthAwarePaginator;
  8. use Webkul\BagistoApi\Models\ProductQuestion;
  9. use Webkul\BagistoApi\Models\ProductQuestionAnswer;
  10. class ProductQuestionProvider implements ProviderInterface
  11. {
  12. public function __construct(
  13. private readonly Pagination $pagination
  14. ) {}
  15. public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
  16. {
  17. $args = $context['args'] ?? [];
  18. $request = request();
  19. $query = ProductQuestion::query();
  20. // Filter by product_id from URI variables (nested route) or query param
  21. $productId = $uriVariables['productId'] ?? $args['product_id'] ?? $request->query('product_id');
  22. if ($productId) {
  23. $query->where('product_id', (int) $productId);
  24. }
  25. // Storefront shows only approved questions by default
  26. $status = $args['status'] ?? $request->query('status', 'approved');
  27. $query->where('status', $status);
  28. $query->with(['customer']);
  29. $query->orderByDesc('created_at');
  30. // ---------------------------------------------------------------
  31. // Embedded answers options
  32. // with_answers=true (default) → embed top N answers per question
  33. // answers_per_question=5 → how many answers to embed (max 20)
  34. // answer_status=approved → which answer status to embed
  35. // ---------------------------------------------------------------
  36. $withAnswers = filter_var(
  37. $args['with_answers'] ?? $request->query('with_answers', 'true'),
  38. FILTER_VALIDATE_BOOLEAN
  39. );
  40. $answersPerQuestion = min(20, max(1, (int) (
  41. $args['answers_per_question'] ?? $request->query('answers_per_question', 5)
  42. )));
  43. $answerStatus = $args['answer_status'] ?? $request->query('answer_status', 'approved');
  44. // REST pagination
  45. $page = max(1, (int) ($request->query('page', 1)));
  46. $perPage = min(50, max(1, (int) ($request->query('per_page', $request->query('limit', 30)))));
  47. // GraphQL cursor pagination
  48. $first = isset($args['first']) ? (int) $args['first'] : null;
  49. $last = isset($args['last']) ? (int) $args['last'] : null;
  50. $after = $args['after'] ?? null;
  51. $before = $args['before'] ?? null;
  52. if ($first || $last || $after || $before) {
  53. $perPage = $first ?? $last ?? 30;
  54. $offset = 0;
  55. if ($after) {
  56. $decoded = base64_decode($after, true);
  57. $offset = ctype_digit((string) $decoded) ? ((int) $decoded + 1) : 0;
  58. }
  59. if ($before) {
  60. $decoded = base64_decode($before, true);
  61. $cursor = ctype_digit((string) $decoded) ? (int) $decoded : 0;
  62. $offset = max(0, $cursor - $perPage);
  63. }
  64. $total = (clone $query)->count();
  65. $items = $query->offset($offset)->limit($perPage)->get();
  66. $currentPage = $total > 0 ? (int) floor($offset / $perPage) + 1 : 1;
  67. } else {
  68. $total = (clone $query)->count();
  69. $offset = ($page - 1) * $perPage;
  70. $items = $query->offset($offset)->limit($perPage)->get();
  71. $currentPage = $page;
  72. }
  73. // Attach answers preview to each question in a single batch query (no N+1)
  74. if ($withAnswers && $items->isNotEmpty()) {
  75. $this->attachAnswersPreview($items, $answersPerQuestion, $answerStatus);
  76. } else {
  77. $items->each(function ($question) {
  78. $question->setAttribute('answers_preview', []);
  79. $question->setAttribute('answers_meta', [
  80. 'total' => $question->answers_count,
  81. 'has_more' => false,
  82. 'page_size' => 0,
  83. ]);
  84. });
  85. }
  86. return new Paginator(
  87. new LengthAwarePaginator(
  88. $items,
  89. $total,
  90. $perPage,
  91. $currentPage,
  92. ['path' => request()->url()]
  93. )
  94. );
  95. }
  96. /**
  97. * Load top N answers for all questions in one query, group by question_id,
  98. * and attach as `answers_preview` + `answers_meta` attributes.
  99. *
  100. * Sort order: pinned first → useful_count desc → newest first.
  101. * This avoids N+1 — only one extra DB query for the whole page.
  102. */
  103. private function attachAnswersPreview(
  104. \Illuminate\Support\Collection $questions,
  105. int $limit,
  106. string $status
  107. ): void {
  108. $questionIds = $questions->pluck('id')->toArray();
  109. // Fetch enough rows to let us take $limit per question after grouping.
  110. // We over-fetch by $limit × question count so we can slice in PHP.
  111. $answers = ProductQuestionAnswer::whereIn('question_id', $questionIds)
  112. ->where('status', $status)
  113. ->orderByDesc('is_pinned')
  114. ->orderByDesc('useful_count')
  115. ->orderByDesc('created_at')
  116. ->get(['id', 'question_id', 'customer_id', 'customer_name', 'answer',
  117. 'status', 'is_pinned', 'useful_count', 'created_at', 'updated_at']);
  118. // Total counts per question (separate lightweight query)
  119. $totalCounts = ProductQuestionAnswer::whereIn('question_id', $questionIds)
  120. ->where('status', $status)
  121. ->groupBy('question_id')
  122. ->selectRaw('question_id, COUNT(*) as cnt')
  123. ->pluck('cnt', 'question_id');
  124. // Group and slice to top N per question
  125. $grouped = $answers->groupBy('question_id')
  126. ->map(fn ($group) => $group->take($limit)->values());
  127. $questions->each(function ($question) use ($grouped, $totalCounts, $limit) {
  128. $preview = $grouped->get($question->id, collect());
  129. $total = (int) ($totalCounts->get($question->id, 0));
  130. $question->setAttribute('answers_preview', $preview->toArray());
  131. $question->setAttribute('answers_meta', [
  132. 'total' => $total,
  133. 'has_more' => $total > $limit,
  134. 'page_size' => $limit,
  135. ]);
  136. });
  137. }
  138. }