perf(settlement,rating): paginate 3 unbounded list endpoints (M10-M12)
- M10 GET /settlement: was unbounded; add page/limit + countByUser + data.meta.
- M11 GET /admin/comments/pending: paginate findPending + countPending.
- M12 GET /comments/{doctor}: paginate the fetch-joined roots query via
Paginator(fetchJoinCollection) + countApprovedRootsByDoctor.
All keep the existing { data: { data: [...] } } envelope and add data.meta
(backward compatible). Default limit 50 / max 100.
Regressions: SettlementListPaginationTest, CommentPaginationTest (both fail
without the limits). Also de-flaked SendCodeMobileRateLimitTest (randomised the
IP block so the persistent per-IP limiter buckets don't accumulate across runs).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -267,19 +267,30 @@ class RatingController extends BaseController
|
||||
]
|
||||
)]
|
||||
#[Route('/api/v1/comments/{doctorUuid}', methods: ['GET'])]
|
||||
public function listComments(string $doctorUuid): JsonResponse
|
||||
public function listComments(string $doctorUuid, Request $request): JsonResponse
|
||||
{
|
||||
$doctor = $this->doctorRepo->findByUuid($doctorUuid);
|
||||
if ($doctor === null) {
|
||||
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'دکتر یافت نشد', 404);
|
||||
}
|
||||
|
||||
$page = max(1, (int) $request->query->get('page', 1));
|
||||
$limit = min(100, max(1, (int) $request->query->get('limit', 50)));
|
||||
|
||||
$comments = array_map(
|
||||
fn(Comment $c) => $c->toArray(),
|
||||
$this->commentRepo->findApprovedRootsByDoctor($doctor)
|
||||
$this->commentRepo->findApprovedRootsByDoctor($doctor, $limit, ($page - 1) * $limit)
|
||||
);
|
||||
$total = $this->commentRepo->countApprovedRootsByDoctor($doctor);
|
||||
|
||||
return $this->success(['data' => $comments]);
|
||||
return $this->success([
|
||||
'data' => $comments,
|
||||
'meta' => [
|
||||
'totalRecords' => $total,
|
||||
'totalPages' => (int) ceil($total / $limit),
|
||||
'currentPage' => $page,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
@@ -347,10 +358,25 @@ class RatingController extends BaseController
|
||||
)]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
#[Route('/api/v1/admin/comments/pending', methods: ['GET'])]
|
||||
public function pendingComments(): JsonResponse
|
||||
public function pendingComments(Request $request): JsonResponse
|
||||
{
|
||||
$comments = array_map(fn(Comment $c) => $c->toArray(), $this->commentRepo->findPending());
|
||||
return $this->success(['data' => $comments]);
|
||||
$page = max(1, (int) $request->query->get('page', 1));
|
||||
$limit = min(100, max(1, (int) $request->query->get('limit', 50)));
|
||||
|
||||
$comments = array_map(
|
||||
fn(Comment $c) => $c->toArray(),
|
||||
$this->commentRepo->findPending($limit, ($page - 1) * $limit)
|
||||
);
|
||||
$total = $this->commentRepo->countPending();
|
||||
|
||||
return $this->success([
|
||||
'data' => $comments,
|
||||
'meta' => [
|
||||
'totalRecords' => $total,
|
||||
'totalPages' => (int) ceil($total / $limit),
|
||||
'currentPage' => $page,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Rating\Repository;
|
||||
use App\Doctor\Entity\Doctor;
|
||||
use App\Rating\Entity\Comment;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
class CommentRepository extends ServiceEntityRepository
|
||||
@@ -19,15 +20,29 @@ class CommentRepository extends ServiceEntityRepository
|
||||
return $this->findBy(['doctor' => $doctor, 'status' => Comment::STATUS_APPROVED], ['createdAt' => 'DESC']);
|
||||
}
|
||||
|
||||
public function countApprovedRootsByDoctor(Doctor $doctor): int
|
||||
{
|
||||
return (int) $this->createQueryBuilder('c')
|
||||
->select('COUNT(c.id)')
|
||||
->where('c.doctor = :doctor')
|
||||
->andWhere('c.status = :status')
|
||||
->andWhere('c.parent IS NULL')
|
||||
->setParameter('doctor', $doctor)
|
||||
->setParameter('status', Comment::STATUS_APPROVED)
|
||||
->getQuery()
|
||||
->getSingleScalarResult();
|
||||
}
|
||||
|
||||
/** @return Comment[] approved root comments (no parent) for a doctor */
|
||||
public function findApprovedRootsByDoctor(Doctor $doctor): array
|
||||
public function findApprovedRootsByDoctor(Doctor $doctor, int $limit = 50, int $offset = 0): array
|
||||
{
|
||||
// Two fetch-join passes (not the product, to avoid a likes×replies
|
||||
// cartesian) hydrate everything toArray() touches, so serialization
|
||||
// triggers no per-comment lazy loads (N+1): pass 1 = roots + author +
|
||||
// likes, pass 2 = replies + their author + likes, merged into the same
|
||||
// managed root entities.
|
||||
$roots = $this->createQueryBuilder('c')
|
||||
// managed root entities. Paginator(fetchJoinCollection) keeps the LIMIT
|
||||
// paginating by root comment, not by joined like rows.
|
||||
$qb = $this->createQueryBuilder('c')
|
||||
->addSelect('u', 'l')
|
||||
->leftJoin('c.user', 'u')
|
||||
->leftJoin('c.likes', 'l')
|
||||
@@ -37,8 +52,10 @@ class CommentRepository extends ServiceEntityRepository
|
||||
->setParameter('doctor', $doctor)
|
||||
->setParameter('status', Comment::STATUS_APPROVED)
|
||||
->orderBy('c.createdAt', 'DESC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
->setFirstResult($offset)
|
||||
->setMaxResults($limit);
|
||||
|
||||
$roots = iterator_to_array(new Paginator($qb, fetchJoinCollection: true));
|
||||
|
||||
if ($roots !== []) {
|
||||
// Also initialise each reply's own replies collection (empty in a
|
||||
@@ -58,10 +75,15 @@ class CommentRepository extends ServiceEntityRepository
|
||||
return $roots;
|
||||
}
|
||||
|
||||
/** @return Comment[] */
|
||||
public function findPending(): array
|
||||
public function countPending(): int
|
||||
{
|
||||
return $this->findBy(['status' => Comment::STATUS_PENDING], ['createdAt' => 'ASC']);
|
||||
return $this->count(['status' => Comment::STATUS_PENDING]);
|
||||
}
|
||||
|
||||
/** @return Comment[] */
|
||||
public function findPending(int $limit = 50, int $offset = 0): array
|
||||
{
|
||||
return $this->findBy(['status' => Comment::STATUS_PENDING], ['createdAt' => 'ASC'], $limit, $offset);
|
||||
}
|
||||
|
||||
public function save(Comment $e, bool $flush = true): void { $this->getEntityManager()->persist($e); if ($flush) $this->getEntityManager()->flush(); }
|
||||
|
||||
@@ -203,14 +203,25 @@ class SettlementController extends BaseController
|
||||
]
|
||||
)]
|
||||
#[Route('/api/v1/settlement', methods: ['GET'])]
|
||||
public function listMine(#[CurrentUser] User $user): JsonResponse
|
||||
public function listMine(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$page = max(1, (int) $request->query->get('page', 1));
|
||||
$limit = min(100, max(1, (int) $request->query->get('limit', 50)));
|
||||
|
||||
$settlements = array_map(
|
||||
fn(Settlement $s) => $s->toArray(),
|
||||
$this->settlementRepo->findByUser($user)
|
||||
$this->settlementRepo->findByUser($user, $limit, ($page - 1) * $limit)
|
||||
);
|
||||
$total = $this->settlementRepo->countByUser($user);
|
||||
|
||||
return $this->success(['data' => $settlements]);
|
||||
return $this->success([
|
||||
'data' => $settlements,
|
||||
'meta' => [
|
||||
'totalRecords' => $total,
|
||||
'totalPages' => (int) ceil($total / $limit),
|
||||
'currentPage' => $page,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
|
||||
@@ -20,9 +20,14 @@ class SettlementRepository extends ServiceEntityRepository
|
||||
}
|
||||
|
||||
/** @return Settlement[] */
|
||||
public function findByUser(User $user): array
|
||||
public function findByUser(User $user, int $limit = 50, int $offset = 0): array
|
||||
{
|
||||
return $this->findBy(['user' => $user], ['createdAt' => 'DESC']);
|
||||
return $this->findBy(['user' => $user], ['createdAt' => 'DESC'], $limit, $offset);
|
||||
}
|
||||
|
||||
public function countByUser(User $user): int
|
||||
{
|
||||
return $this->count(['user' => $user]);
|
||||
}
|
||||
|
||||
/** Balance = sum of credits - sum of debits from wallet_transactions */
|
||||
|
||||
Reference in New Issue
Block a user