issues/27: old meta with pagination and trait and more
This commit is contained in:
committed by
Valeriy Petrov
parent
bc5468e5a0
commit
656f79ff4e
@@ -28,7 +28,7 @@ final class ArticleController extends AbstractController
|
||||
|
||||
#[OA\Tag(name: 'Статьи')]
|
||||
#[OA\Parameter(name: 'page', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
||||
#[OA\Parameter(name: 'perPage', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
||||
#[OA\Parameter(name: 'limit', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
||||
#[OA\Parameter(name: 'regionId', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
||||
#[OA\Parameter(name: 'active', in: 'query', schema: new OA\Schema(type: 'boolean'))]
|
||||
#[OA\Parameter(name: 'alias', in: 'query', schema: new OA\Schema(type: 'string'))]
|
||||
@@ -38,7 +38,7 @@ final class ArticleController extends AbstractController
|
||||
{
|
||||
$qb = $repository->createFilteredQueryBuilder($request->query->all());
|
||||
|
||||
return $this->json($this->paginator->paginate($qb, $request), Response::HTTP_OK, [], [
|
||||
return $this->json($this->paginator->paginateWithLegacyMeta($qb, $request), Response::HTTP_OK, [], [
|
||||
'groups' => self::READ_GROUPS,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ use Doctrine\Persistence\ManagerRegistry;
|
||||
*/
|
||||
class ArticleRepository extends ServiceEntityRepository
|
||||
{
|
||||
use ContentFilterTrait;
|
||||
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Article::class);
|
||||
@@ -24,7 +26,7 @@ class ArticleRepository extends ServiceEntityRepository
|
||||
{
|
||||
$qb = $this->createQueryBuilder('a')->orderBy('a.id', 'DESC');
|
||||
|
||||
ContentRepositoryFilter::applyCommon($qb, 'a', $filters);
|
||||
$this->applyCommonFilters($qb, 'a', $filters);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
+21
-17
@@ -9,43 +9,41 @@ use Doctrine\ORM\QueryBuilder;
|
||||
/**
|
||||
* Общие фильтры для контентных репозиториев (News/Promo/Disease/MedicalCenter/Article/SiteService).
|
||||
*
|
||||
* Назначение — не плодить одинаковые if-блоки в каждом репозитории и сохранить
|
||||
* единый контракт query-параметров для list-эндпоинтов.
|
||||
* Trait подключается в Doctrine-репозитории, чтобы не держать бизнес-фильтры
|
||||
* в статическом helper-классе и при этом не копировать одинаковые if-блоки.
|
||||
*
|
||||
* Поддерживается:
|
||||
* - regionId целое > 0
|
||||
* - active bool (по умолчанию true, если не передан)
|
||||
* - alias строка (строгое равенство)
|
||||
* - search LIKE по name (case-insensitive)
|
||||
*
|
||||
* Параметры пагинации (page/perPage) обрабатываются Paginator'ом и здесь игнорируются.
|
||||
* - regionId / region_id: целое > 0;
|
||||
* - active: bool;
|
||||
* - alias: точное совпадение;
|
||||
* - search / q: LIKE по name в lower-case.
|
||||
*/
|
||||
final class ContentRepositoryFilter
|
||||
trait ContentFilterTrait
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $filters
|
||||
*/
|
||||
public static function applyCommon(QueryBuilder $qb, string $alias, array $filters): void
|
||||
private function applyCommonFilters(QueryBuilder $qb, string $alias, array $filters): void
|
||||
{
|
||||
$regionId = self::extractInt($filters, ['regionId', 'region_id']);
|
||||
$regionId = $this->extractIntFilter($filters, ['regionId', 'region_id']);
|
||||
if ($regionId !== null && $regionId > 0) {
|
||||
$qb->andWhere("$alias.regionId = :regionId")
|
||||
->setParameter('regionId', $regionId);
|
||||
}
|
||||
|
||||
$active = self::extractBool($filters, ['active']);
|
||||
$active = $this->extractBoolFilter($filters, ['active']);
|
||||
if ($active !== null) {
|
||||
$qb->andWhere("$alias.active = :active")
|
||||
->setParameter('active', $active);
|
||||
}
|
||||
|
||||
$aliasFilter = self::extractNonEmptyString($filters, ['alias']);
|
||||
$aliasFilter = $this->extractNonEmptyStringFilter($filters, ['alias']);
|
||||
if ($aliasFilter !== null) {
|
||||
$qb->andWhere("$alias.alias = :aliasValue")
|
||||
->setParameter('aliasValue', $aliasFilter);
|
||||
}
|
||||
|
||||
$search = self::extractNonEmptyString($filters, ['search', 'q']);
|
||||
$search = $this->extractNonEmptyStringFilter($filters, ['search', 'q']);
|
||||
if ($search !== null) {
|
||||
$qb->andWhere("LOWER($alias.name) LIKE :search")
|
||||
->setParameter('search', '%' . mb_strtolower($search) . '%');
|
||||
@@ -56,16 +54,18 @@ final class ContentRepositoryFilter
|
||||
* @param array<string, mixed> $filters
|
||||
* @param list<string> $keys
|
||||
*/
|
||||
private static function extractInt(array $filters, array $keys): ?int
|
||||
private function extractIntFilter(array $filters, array $keys): ?int
|
||||
{
|
||||
foreach ($keys as $key) {
|
||||
if (!array_key_exists($key, $filters)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $filters[$key];
|
||||
if ($value === null || $value === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_numeric($value)) {
|
||||
return (int) $value;
|
||||
}
|
||||
@@ -78,16 +78,18 @@ final class ContentRepositoryFilter
|
||||
* @param array<string, mixed> $filters
|
||||
* @param list<string> $keys
|
||||
*/
|
||||
private static function extractBool(array $filters, array $keys): ?bool
|
||||
private function extractBoolFilter(array $filters, array $keys): ?bool
|
||||
{
|
||||
foreach ($keys as $key) {
|
||||
if (!array_key_exists($key, $filters)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $filters[$key];
|
||||
if ($value === null || $value === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_bool($value)) {
|
||||
return $value;
|
||||
}
|
||||
@@ -102,16 +104,18 @@ final class ContentRepositoryFilter
|
||||
* @param array<string, mixed> $filters
|
||||
* @param list<string> $keys
|
||||
*/
|
||||
private static function extractNonEmptyString(array $filters, array $keys): ?string
|
||||
private function extractNonEmptyStringFilter(array $filters, array $keys): ?string
|
||||
{
|
||||
foreach ($keys as $key) {
|
||||
if (!array_key_exists($key, $filters)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $filters[$key];
|
||||
if (!is_string($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$trimmed = trim($value);
|
||||
if ($trimmed !== '') {
|
||||
return $trimmed;
|
||||
@@ -15,6 +15,8 @@ use Doctrine\Persistence\ManagerRegistry;
|
||||
*/
|
||||
class DiseaseRepository extends ServiceEntityRepository
|
||||
{
|
||||
use ContentFilterTrait;
|
||||
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Disease::class);
|
||||
@@ -27,7 +29,7 @@ class DiseaseRepository extends ServiceEntityRepository
|
||||
{
|
||||
$qb = $this->createQueryBuilder('d')->orderBy('d.id', 'ASC');
|
||||
|
||||
ContentRepositoryFilter::applyCommon($qb, 'd', $filters);
|
||||
$this->applyCommonFilters($qb, 'd', $filters);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ use Doctrine\Persistence\ManagerRegistry;
|
||||
*/
|
||||
class MedicalCenterRepository extends ServiceEntityRepository
|
||||
{
|
||||
use ContentFilterTrait;
|
||||
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, MedicalCenter::class);
|
||||
@@ -27,7 +29,7 @@ class MedicalCenterRepository extends ServiceEntityRepository
|
||||
{
|
||||
$qb = $this->createQueryBuilder('m')->orderBy('m.id', 'DESC');
|
||||
|
||||
ContentRepositoryFilter::applyCommon($qb, 'm', $filters);
|
||||
$this->applyCommonFilters($qb, 'm', $filters);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ use Doctrine\Persistence\ManagerRegistry;
|
||||
*/
|
||||
class NewsRepository extends ServiceEntityRepository
|
||||
{
|
||||
use ContentFilterTrait;
|
||||
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, News::class);
|
||||
@@ -31,7 +33,7 @@ class NewsRepository extends ServiceEntityRepository
|
||||
{
|
||||
$qb = $this->createQueryBuilder('n')->orderBy('n.id', 'DESC');
|
||||
|
||||
ContentRepositoryFilter::applyCommon($qb, 'n', $filters);
|
||||
$this->applyCommonFilters($qb, 'n', $filters);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ use Doctrine\Persistence\ManagerRegistry;
|
||||
*/
|
||||
class PromoRepository extends ServiceEntityRepository
|
||||
{
|
||||
use ContentFilterTrait;
|
||||
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Promo::class);
|
||||
@@ -27,7 +29,7 @@ class PromoRepository extends ServiceEntityRepository
|
||||
{
|
||||
$qb = $this->createQueryBuilder('p')->orderBy('p.id', 'DESC');
|
||||
|
||||
ContentRepositoryFilter::applyCommon($qb, 'p', $filters);
|
||||
$this->applyCommonFilters($qb, 'p', $filters);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ use Doctrine\Persistence\ManagerRegistry;
|
||||
*/
|
||||
class SiteServiceRepository extends ServiceEntityRepository
|
||||
{
|
||||
use ContentFilterTrait;
|
||||
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, SiteService::class);
|
||||
@@ -27,7 +29,7 @@ class SiteServiceRepository extends ServiceEntityRepository
|
||||
{
|
||||
$qb = $this->createQueryBuilder('s')->orderBy('s.id', 'ASC');
|
||||
|
||||
ContentRepositoryFilter::applyCommon($qb, 's', $filters);
|
||||
$this->applyCommonFilters($qb, 's', $filters);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Service\Crud;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use JsonException;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -142,9 +143,11 @@ final class CrudResponder
|
||||
*/
|
||||
private function decodePayload(Request $request): ?array
|
||||
{
|
||||
$data = json_decode($request->getContent(), true);
|
||||
|
||||
return is_array($data) ? $data : null;
|
||||
try {
|
||||
return $request->toArray();
|
||||
} catch (JsonException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function validate(object $entity): ?JsonResponse
|
||||
|
||||
@@ -15,7 +15,7 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
*
|
||||
* Соответствует существующему стилю проекта (см. PriceListController/SpecialistController):
|
||||
* читает page/perPage из Request, ограничивает perPage и возвращает массив
|
||||
* ['data' => [...], 'pagination' => [...]] в едином формате для всех контроллеров.
|
||||
* ['data' => [...], 'pagination' => [...]] в едином формате для новых list-контрактов.
|
||||
*/
|
||||
final class Paginator
|
||||
{
|
||||
@@ -62,4 +62,46 @@ final class Paginator
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy-формат для ArticleController.
|
||||
*
|
||||
* Старый контракт /article/list уже использовался клиентами:
|
||||
* - размер страницы приходит в query-параметре limit;
|
||||
* - метаданные лежат в ключе meta;
|
||||
* - поля называются total/page/limit/totalPages.
|
||||
*
|
||||
* @return array{data: list<mixed>, meta: array{total: int, page: int, limit: int, totalPages: int}}
|
||||
*/
|
||||
public function paginateWithLegacyMeta(
|
||||
QueryBuilder $qb,
|
||||
Request $request,
|
||||
int $defaultLimit = 20,
|
||||
int $maxLimit = 100,
|
||||
): array {
|
||||
$page = max(1, $request->query->getInt('page', 1));
|
||||
$limit = min(
|
||||
max(1, $request->query->getInt('limit', $defaultLimit)),
|
||||
$maxLimit,
|
||||
);
|
||||
|
||||
$pagerfanta = (new Pagerfanta(new QueryAdapter($qb)))
|
||||
->setMaxPerPage($limit);
|
||||
|
||||
try {
|
||||
$pagerfanta->setCurrentPage($page);
|
||||
} catch (NotValidCurrentPageException) {
|
||||
$pagerfanta->setCurrentPage(max(1, $pagerfanta->getNbPages()));
|
||||
}
|
||||
|
||||
return [
|
||||
'data' => iterator_to_array($pagerfanta->getCurrentPageResults(), false),
|
||||
'meta' => [
|
||||
'total' => $pagerfanta->getNbResults(),
|
||||
'page' => $pagerfanta->getCurrentPage(),
|
||||
'limit' => $pagerfanta->getMaxPerPage(),
|
||||
'totalPages' => $pagerfanta->getNbPages(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user