@@ -397,6 +397,9 @@ public function insertOrUpdate(object $entity): object {
397397 * @param array<string, int|float|string|null|\DateTime|\BackedEnum|list<int|float|string|\BackedEnum>> $criteria
398398 * @param array<string, \SortDirection> $orderBy
399399 * @return \Generator<T>
400+ *
401+ * @note If you need to implement pagination, prefer using findByAfterId instead.
402+ *
400403 * @since 35.0.0
401404 */
402405 public function findBy (array $ criteria , array $ orderBy = [], ?int $ limit = null , ?int $ offset = null ): \Generator {
@@ -413,6 +416,78 @@ public function findBy(array $criteria, array $orderBy = [], ?int $limit = null,
413416 return $ this ->yieldJoinedEntities ($ qb , $ relations );
414417 }
415418
419+ /**
420+ * Finds entities by a set of criteria, keyed by property name, one page at a time ordered by
421+ * their primary key — using keyset (seek) pagination instead of OFFSET/LIMIT.
422+ *
423+ * Unlike findBy()'s $offset, which forces the database to scan and discard every preceding
424+ * row on every call, $lastId lets it seek straight to the right spot through the primary
425+ * key's index, so each page costs the same regardless of how deep it is. Pass null to fetch
426+ * the first page, then the id of the last entity returned to fetch the next one; stop once
427+ * fewer than $limit entities come back.
428+ *
429+ * @warning This does not support tables with composite primary keys
430+ *
431+ * @param array<string, int|float|string|null|\DateTime|\BackedEnum|list<int|float|string|\BackedEnum>> $criteria
432+ * @param int|string|null $lastId The primary key of the last entity from the previous
433+ * page, or null to fetch the first page.
434+ * @return \Generator<T>
435+ * @throws \LogicException if the entity has a composite primary key
436+ * @since 35.0.0
437+ */
438+ public function findByAfterId (array $ criteria , int |float |string |null $ lastId , int $ limit ): \Generator {
439+ $ entityInfo = $ this ->entityManager ->getEntityInfo ($ this ->getEntityClass ());
440+ $ idColumn = $ entityInfo ->mappingPropertyToColumn [$ entityInfo ->getSingleIdProperty ()->getName ()];
441+
442+ [$ qb , $ relations ] = $ this ->getJoinedSelectQueryBuilder ($ criteria );
443+
444+ if ($ lastId !== null ) {
445+ $ type = $ this ->entityManager ->getParameterType ($ entityInfo ->mappingColumnToTypes [$ idColumn ], false );
446+ $ qb ->andWhere ($ qb ->expr ()->gt ('e. ' . $ idColumn , $ qb ->createNamedParameter ($ lastId , $ type )));
447+ }
448+
449+ $ qb ->orderBy ('e. ' . $ idColumn , \SortDirection::Ascending);
450+ $ qb ->setMaxResults ($ limit );
451+
452+ return $ this ->yieldJoinedEntities ($ qb , $ relations );
453+ }
454+
455+ /**
456+ * Finds entities by a set of criteria, keyed by property name, one page at a time ordered by
457+ * their primary key in descending order — using keyset (seek) pagination instead of
458+ * OFFSET/LIMIT.
459+ *
460+ * This is the mirror image of findByAfterId(), walking from the highest id downwards instead
461+ * of from the lowest id upwards. Pass null to fetch the first page (starting from the highest
462+ * id), then the id of the last entity returned to fetch the next one; stop once fewer than
463+ * $limit entities come back.
464+ *
465+ * @warning This does not support tables with composite primary keys
466+ *
467+ * @param array<string, int|float|string|null|\DateTime|\BackedEnum|list<int|float|string|\BackedEnum>> $criteria
468+ * @param int|string|null $lastId The primary key of the last entity from the previous
469+ * page, or null to fetch the first page.
470+ * @return \Generator<T>
471+ * @throws \LogicException if the entity has a composite primary key
472+ * @since 35.0.0
473+ */
474+ public function findByBeforeId (array $ criteria , int |float |string |null $ lastId , int $ limit ): \Generator {
475+ $ entityInfo = $ this ->entityManager ->getEntityInfo ($ this ->getEntityClass ());
476+ $ idColumn = $ entityInfo ->mappingPropertyToColumn [$ entityInfo ->getSingleIdProperty ()->getName ()];
477+
478+ [$ qb , $ relations ] = $ this ->getJoinedSelectQueryBuilder ($ criteria );
479+
480+ if ($ lastId !== null ) {
481+ $ type = $ this ->entityManager ->getParameterType ($ entityInfo ->mappingColumnToTypes [$ idColumn ], false );
482+ $ qb ->andWhere ($ qb ->expr ()->lt ('e. ' . $ idColumn , $ qb ->createNamedParameter ($ lastId , $ type )));
483+ }
484+
485+ $ qb ->orderBy ('e. ' . $ idColumn , \SortDirection::Descending);
486+ $ qb ->setMaxResults ($ limit );
487+
488+ return $ this ->yieldJoinedEntities ($ qb , $ relations );
489+ }
490+
416491 /**
417492 * @param array<string, int|float|string|null|\DateTime|\BackedEnum|list<int|float|string|\BackedEnum>> $criteria
418493 * @return int The number of rows deleted
0 commit comments