From bc5468e5a078bc853d4f926d8465d44663f2d709 Mon Sep 17 00:00:00 2001 From: Valery Petrov Date: Thu, 14 May 2026 16:16:07 +0300 Subject: [PATCH] issues/27: update crud from admin api --- config/packages/nelmio_api_doc.yaml | 10 +- src/Controller/ArticleController.php | 129 ++----- src/Controller/DiseaseController.php | 80 ++-- src/Controller/MedicalCenterController.php | 62 ++- src/Controller/NewsController.php | 62 ++- src/Controller/PromoController.php | 62 ++- src/Controller/SiteServiceController.php | 81 ++-- src/Repository/ArticleRepository.php | 59 +-- src/Repository/ContentRepositoryFilter.php | 123 ++++++ src/Repository/DiseaseRepository.php | 13 + src/Repository/MedicalCenterRepository.php | 13 + src/Repository/NewsRepository.php | 17 + src/Repository/PromoRepository.php | 13 + src/Repository/SiteServiceRepository.php | 13 + src/Service/Crud/CrudResponder.php | 184 +++++++++ src/Service/DiseaseCrudService.php | 192 +--------- src/Service/MedicalCenterCrudService.php | 417 ++++++--------------- src/Service/NewsCrudService.php | 136 +------ src/Service/Pagination/Paginator.php | 65 ++++ src/Service/PromoCrudService.php | 134 +------ src/Service/SiteServiceCrudService.php | 346 +---------------- 21 files changed, 755 insertions(+), 1456 deletions(-) create mode 100644 src/Repository/ContentRepositoryFilter.php create mode 100644 src/Service/Crud/CrudResponder.php create mode 100644 src/Service/Pagination/Paginator.php diff --git a/config/packages/nelmio_api_doc.yaml b/config/packages/nelmio_api_doc.yaml index 26c60a8..af132f8 100644 --- a/config/packages/nelmio_api_doc.yaml +++ b/config/packages/nelmio_api_doc.yaml @@ -16,5 +16,11 @@ nelmio_api_doc: '^/specialist/list$', '^/specialist/schedule$', '^/pricelist/list$', - '^/pricelist/department$' - ] \ No newline at end of file + '^/pricelist/department$', + '^/news($|/)', + '^/promo($|/)', + '^/disease($|/)', + '^/medical-center($|/)', + '^/article($|/)', + '^/site-services($|/)' + ] diff --git a/src/Controller/ArticleController.php b/src/Controller/ArticleController.php index ef454fe..6b701bf 100644 --- a/src/Controller/ArticleController.php +++ b/src/Controller/ArticleController.php @@ -4,52 +4,42 @@ namespace App\Controller; use App\Entity\Article; use App\Repository\ArticleRepository; -use Doctrine\ORM\EntityManagerInterface; +use App\Service\Crud\CrudResponder; +use App\Service\Pagination\Paginator; +use OpenApi\Attributes as OA; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; -use Symfony\Component\Serializer\SerializerInterface; -use Symfony\Component\Validator\Validator\ValidatorInterface; -use Exception; #[Route('/article')] final class ArticleController extends AbstractController { - public function __construct( - private EntityManagerInterface $em, - private ValidatorInterface $validator, - private SerializerInterface $serializer - ) { } + private const READ_GROUPS = ['article:read']; + private const WRITE_GROUPS = ['article:write']; + public function __construct( + private readonly CrudResponder $crud, + private readonly Paginator $paginator, + ) { + } + + #[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: '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'))] + #[OA\Parameter(name: 'search', in: 'query', schema: new OA\Schema(type: 'string'))] #[Route('/list', name: 'article_list', methods: ['GET'])] public function list(Request $request, ArticleRepository $repository): JsonResponse { - $page = max(1, (int) $request->query->get('page', 1)); - $limit = min(100, max(1, (int) $request->query->get('limit', 20))); + $qb = $repository->createFilteredQueryBuilder($request->query->all()); - $filters = [ - 'alias' => $request->query->get('alias', ''), - 'active' => $request->query->get('active', ''), - 'regionId' => $request->query->get('regionId', ''), - ]; - - $articles = $repository->findByFilters($filters, $page, $limit); - $total = $repository->countByFilters($filters); - $totalPages = (int) ceil($total / $limit); - - return $this->json([ - 'data' => $articles, - 'meta' => [ - 'total' => $total, - 'page' => $page, - 'limit' => $limit, - 'totalPages' => $totalPages, - ], - ], Response::HTTP_OK, [], [ - 'groups' => ['article:read'] + return $this->json($this->paginator->paginate($qb, $request), Response::HTTP_OK, [], [ + 'groups' => self::READ_GROUPS, ]); } @@ -60,99 +50,34 @@ final class ArticleController extends AbstractController if (!$article) { throw $this->createNotFoundException('Статья не найдена'); } - return $this->json($article, Response::HTTP_OK, [], [ - 'groups' => ['article:read'] - ]); + + return $this->crud->read($article, self::READ_GROUPS); } #[Route('/{id}', name: 'article_show', methods: ['GET'], requirements: ['id' => '\d+'])] public function show(Article $article): JsonResponse { - return $this->json($article, Response::HTTP_OK, [], [ - 'groups' => ['article:read'] - ]); + return $this->crud->read($article, self::READ_GROUPS); } #[IsGranted('ROLE_ADMIN')] #[Route('/create', name: 'article_create', methods: ['POST'])] public function create(Request $request): JsonResponse { - try { - $article = $this->serializer->deserialize( - $request->getContent(), - Article::class, - 'json', - ['groups' => ['article:write']] - ); - - $errors = $this->validator->validate($article); - - if (count($errors) > 0) { - return $this->json($errors, Response::HTTP_BAD_REQUEST); - } - - $this->em->persist($article); - $this->em->flush(); - - return $this->json($article, Response::HTTP_CREATED, [], [ - 'groups' => ['article:read'] - ]); - } catch (Exception $e) { - return new JsonResponse([ - 'error' => 'Ошибка при создании статьи', - 'message' => $e->getMessage() - ], Response::HTTP_INTERNAL_SERVER_ERROR); - } + return $this->crud->create($request, Article::class, self::WRITE_GROUPS, self::READ_GROUPS); } #[IsGranted('ROLE_ADMIN')] #[Route('/{id}', name: 'article_update', methods: ['PUT'], requirements: ['id' => '\d+'])] public function update(Request $request, Article $article): JsonResponse { - try { - $this->serializer->deserialize( - $request->getContent(), - Article::class, - 'json', - [ - 'groups' => ['article:write'], - 'object_to_populate' => $article - ] - ); - - $errors = $this->validator->validate($article); - - if (count($errors) > 0) { - return $this->json($errors, Response::HTTP_BAD_REQUEST); - } - - $this->em->flush(); - - return $this->json($article, Response::HTTP_OK, [], [ - 'groups' => ['article:read'] - ]); - } catch (Exception $e) { - return new JsonResponse([ - 'error' => 'Ошибка при обновлении статьи', - 'message' => $e->getMessage() - ], Response::HTTP_INTERNAL_SERVER_ERROR); - } + return $this->crud->update($request, $article, self::WRITE_GROUPS, self::READ_GROUPS); } #[IsGranted('ROLE_ADMIN')] #[Route('/{id}', name: 'article_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])] public function delete(Article $article): JsonResponse { - try { - $this->em->remove($article); - $this->em->flush(); - - return new JsonResponse(null, Response::HTTP_NO_CONTENT); - } catch (Exception $e) { - return new JsonResponse([ - 'error' => 'Ошибка при удалении статьи', - 'message' => $e->getMessage() - ], Response::HTTP_INTERNAL_SERVER_ERROR); - } + return $this->crud->delete($article); } } diff --git a/src/Controller/DiseaseController.php b/src/Controller/DiseaseController.php index 04b4ce0..43e10cb 100644 --- a/src/Controller/DiseaseController.php +++ b/src/Controller/DiseaseController.php @@ -3,7 +3,10 @@ namespace App\Controller; use App\Entity\Disease; -use App\Service\DiseaseCrudService; +use App\Repository\DiseaseRepository; +use App\Service\Crud\CrudResponder; +use App\Service\Pagination\Paginator; +use OpenApi\Attributes as OA; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -14,90 +17,55 @@ use Symfony\Component\Security\Http\Attribute\IsGranted; #[Route('/disease')] final class DiseaseController extends AbstractController { + private const READ_GROUPS = ['disease:read']; + private const WRITE_GROUPS = ['disease:write']; + public function __construct( - private DiseaseCrudService $diseaseCrud, + private readonly CrudResponder $crud, + private readonly Paginator $paginator, ) { } + #[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: 'regionId', in: 'query', schema: new OA\Schema(type: 'integer'))] + #[OA\Parameter(name: 'active', in: 'query', schema: new OA\Schema(type: 'boolean'))] + #[OA\Parameter(name: 'search', in: 'query', schema: new OA\Schema(type: 'string'))] #[Route('/list', name: 'disease_list', methods: ['GET'])] - public function list(Request $request): JsonResponse + public function list(Request $request, DiseaseRepository $repository): JsonResponse { - $page = $request->query->getInt('page', 1); - $perPage = min($request->query->getInt('perPage', 100), 500); - $regionId = $request->query->getInt('regionId', 0) ?: null; + $qb = $repository->createFilteredQueryBuilder($request->query->all()); - $result = $this->diseaseCrud->getPaginatedList($page, $perPage, $regionId); - $data = $result['data']; - $total = $result['total']; - $perPage = $result['per_page']; - $totalPages = (int) ceil($total / $perPage); - - return $this->json([ - 'data' => $data, - 'pagination' => [ - 'total' => $total, - 'count' => count($data), - 'per_page' => $perPage, - 'current_page' => $result['page'], - 'total_pages' => $totalPages, - 'has_previous_page' => $result['page'] > 1, - 'has_next_page' => $result['page'] < $totalPages, - ], - ], Response::HTTP_OK, [], [ - 'groups' => ['disease:read'], + return $this->json($this->paginator->paginate($qb, $request), Response::HTTP_OK, [], [ + 'groups' => self::READ_GROUPS, ]); } #[Route('/{id}', name: 'disease_show', methods: ['GET'], requirements: ['id' => '\d+'])] public function show(Disease $disease): JsonResponse { - return $this->json($disease, Response::HTTP_OK, [], [ - 'groups' => ['disease:read'], - ]); + return $this->crud->read($disease, self::READ_GROUPS); } #[IsGranted('ROLE_ADMIN')] #[Route('/create', name: 'disease_create', methods: ['POST'])] public function create(Request $request): JsonResponse { - $data = json_decode($request->getContent(), true); - if (!is_array($data)) { - return $this->json(['error' => 'Ожидается JSON-объект в теле запроса'], Response::HTTP_BAD_REQUEST); - } - - try { - $disease = $this->diseaseCrud->create($data); - } catch (\InvalidArgumentException $e) { - return $this->json(['error' => $e->getMessage()], Response::HTTP_BAD_REQUEST); - } - - return $this->json($disease, Response::HTTP_CREATED, [], [ - 'groups' => ['disease:read'], - ]); + return $this->crud->create($request, Disease::class, self::WRITE_GROUPS, self::READ_GROUPS); } #[IsGranted('ROLE_ADMIN')] #[Route('/{id}', name: 'disease_update', methods: ['PUT'], requirements: ['id' => '\d+'])] - public function update(Disease $disease, Request $request): JsonResponse + public function update(Request $request, Disease $disease): JsonResponse { - $data = json_decode($request->getContent(), true); - if (!is_array($data)) { - return $this->json(['error' => 'Ожидается JSON-объект в теле запроса'], Response::HTTP_BAD_REQUEST); - } - - $disease = $this->diseaseCrud->update($disease, $data); - - return $this->json($disease, Response::HTTP_OK, [], [ - 'groups' => ['disease:read'], - ]); + return $this->crud->update($request, $disease, self::WRITE_GROUPS, self::READ_GROUPS); } #[IsGranted('ROLE_ADMIN')] #[Route('/{id}', name: 'disease_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])] public function delete(Disease $disease): JsonResponse { - $this->diseaseCrud->delete($disease); - - return new JsonResponse(null, Response::HTTP_NO_CONTENT); + return $this->crud->delete($disease); } } diff --git a/src/Controller/MedicalCenterController.php b/src/Controller/MedicalCenterController.php index 0492347..ad53c4e 100644 --- a/src/Controller/MedicalCenterController.php +++ b/src/Controller/MedicalCenterController.php @@ -3,7 +3,10 @@ namespace App\Controller; use App\Entity\MedicalCenter; -use App\Service\MedicalCenterCrudService; +use App\Repository\MedicalCenterRepository; +use App\Service\Crud\CrudResponder; +use App\Service\Pagination\Paginator; +use OpenApi\Attributes as OA; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -14,72 +17,55 @@ use Symfony\Component\Security\Http\Attribute\IsGranted; #[Route('/medical-center')] final class MedicalCenterController extends AbstractController { + private const READ_GROUPS = ['medical_center:read']; + private const WRITE_GROUPS = ['medical_center:write']; + public function __construct( - private MedicalCenterCrudService $medicalCenterCrud, + private readonly CrudResponder $crud, + private readonly Paginator $paginator, ) { } + #[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: 'regionId', in: 'query', schema: new OA\Schema(type: 'integer'))] + #[OA\Parameter(name: 'active', in: 'query', schema: new OA\Schema(type: 'boolean'))] + #[OA\Parameter(name: 'search', in: 'query', schema: new OA\Schema(type: 'string'))] #[Route('/list', name: 'medical_center_list', methods: ['GET'])] - public function list(Request $request): JsonResponse + public function list(Request $request, MedicalCenterRepository $repository): JsonResponse { - $regionId = $request->query->getInt('regionId', 0); - $activeParam = $request->query->get('active'); - $active = $activeParam === null ? true : filter_var($activeParam, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); - if ($activeParam !== null && $active === null) { - return $this->json(['error' => 'Параметр active должен быть boolean'], Response::HTTP_BAD_REQUEST); - } + $qb = $repository->createFilteredQueryBuilder($request->query->all()); - return $this->json(['data' => $this->medicalCenterCrud->getList($regionId > 0 ? $regionId : null, $active)], Response::HTTP_OK, [], [ - 'groups' => ['medical_center:read'], + return $this->json($this->paginator->paginate($qb, $request), Response::HTTP_OK, [], [ + 'groups' => self::READ_GROUPS, ]); } #[Route('/{id}', name: 'medical_center_show', methods: ['GET'], requirements: ['id' => '\d+'])] public function show(MedicalCenter $medicalCenter): JsonResponse { - return $this->json($medicalCenter, Response::HTTP_OK, [], [ - 'groups' => ['medical_center:read'], - ]); + return $this->crud->read($medicalCenter, self::READ_GROUPS); } #[IsGranted('ROLE_ADMIN')] #[Route('/create', name: 'medical_center_create', methods: ['POST'])] public function create(Request $request): JsonResponse { - $data = json_decode($request->getContent(), true); - if (!is_array($data)) { - return $this->json(['error' => 'Ожидается JSON-объект в теле запроса'], Response::HTTP_BAD_REQUEST); - } - - $medicalCenter = $this->medicalCenterCrud->create($data); - - return $this->json($medicalCenter, Response::HTTP_CREATED, [], [ - 'groups' => ['medical_center:read'], - ]); + return $this->crud->create($request, MedicalCenter::class, self::WRITE_GROUPS, self::READ_GROUPS); } #[IsGranted('ROLE_ADMIN')] #[Route('/{id}', name: 'medical_center_update', methods: ['PUT'], requirements: ['id' => '\d+'])] - public function update(MedicalCenter $medicalCenter, Request $request): JsonResponse + public function update(Request $request, MedicalCenter $medicalCenter): JsonResponse { - $data = json_decode($request->getContent(), true); - if (!is_array($data)) { - return $this->json(['error' => 'Ожидается JSON-объект в теле запроса'], Response::HTTP_BAD_REQUEST); - } - - $medicalCenter = $this->medicalCenterCrud->update($medicalCenter, $data); - - return $this->json($medicalCenter, Response::HTTP_OK, [], [ - 'groups' => ['medical_center:read'], - ]); + return $this->crud->update($request, $medicalCenter, self::WRITE_GROUPS, self::READ_GROUPS); } #[IsGranted('ROLE_ADMIN')] #[Route('/{id}', name: 'medical_center_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])] public function delete(MedicalCenter $medicalCenter): JsonResponse { - $this->medicalCenterCrud->delete($medicalCenter); - - return new JsonResponse(null, Response::HTTP_NO_CONTENT); + return $this->crud->delete($medicalCenter); } } diff --git a/src/Controller/NewsController.php b/src/Controller/NewsController.php index c0c8c4e..96f4534 100644 --- a/src/Controller/NewsController.php +++ b/src/Controller/NewsController.php @@ -3,7 +3,10 @@ namespace App\Controller; use App\Entity\News; -use App\Service\NewsCrudService; +use App\Repository\NewsRepository; +use App\Service\Crud\CrudResponder; +use App\Service\Pagination\Paginator; +use OpenApi\Attributes as OA; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -14,72 +17,55 @@ use Symfony\Component\Security\Http\Attribute\IsGranted; #[Route('/news')] final class NewsController extends AbstractController { + private const READ_GROUPS = ['news:read']; + private const WRITE_GROUPS = ['news:write']; + public function __construct( - private NewsCrudService $newsCrud, + private readonly CrudResponder $crud, + private readonly Paginator $paginator, ) { } + #[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: 'regionId', in: 'query', schema: new OA\Schema(type: 'integer'))] + #[OA\Parameter(name: 'active', in: 'query', schema: new OA\Schema(type: 'boolean'))] + #[OA\Parameter(name: 'search', in: 'query', schema: new OA\Schema(type: 'string'))] #[Route('/list', name: 'news_list', methods: ['GET'])] - public function list(Request $request): JsonResponse + public function list(Request $request, NewsRepository $repository): JsonResponse { - $regionId = $request->query->getInt('regionId', 0); - $activeParam = $request->query->get('active'); - $active = $activeParam === null ? true : filter_var($activeParam, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); - if ($activeParam !== null && $active === null) { - return $this->json(['error' => 'Параметр active должен быть boolean'], Response::HTTP_BAD_REQUEST); - } + $qb = $repository->createFilteredQueryBuilder($request->query->all()); - return $this->json(['data' => $this->newsCrud->getList($regionId > 0 ? $regionId : null, $active)], Response::HTTP_OK, [], [ - 'groups' => ['news:read'], + return $this->json($this->paginator->paginate($qb, $request), Response::HTTP_OK, [], [ + 'groups' => self::READ_GROUPS, ]); } #[Route('/{id}', name: 'news_show', methods: ['GET'], requirements: ['id' => '\d+'])] public function show(News $news): JsonResponse { - return $this->json($news, Response::HTTP_OK, [], [ - 'groups' => ['news:read'], - ]); + return $this->crud->read($news, self::READ_GROUPS); } #[IsGranted('ROLE_ADMIN')] #[Route('/create', name: 'news_create', methods: ['POST'])] public function create(Request $request): JsonResponse { - $data = json_decode($request->getContent(), true); - if (!is_array($data)) { - return $this->json(['error' => 'Ожидается JSON-объект в теле запроса'], Response::HTTP_BAD_REQUEST); - } - - $news = $this->newsCrud->create($data); - - return $this->json($news, Response::HTTP_CREATED, [], [ - 'groups' => ['news:read'], - ]); + return $this->crud->create($request, News::class, self::WRITE_GROUPS, self::READ_GROUPS); } #[IsGranted('ROLE_ADMIN')] #[Route('/{id}', name: 'news_update', methods: ['PUT'], requirements: ['id' => '\d+'])] - public function update(News $news, Request $request): JsonResponse + public function update(Request $request, News $news): JsonResponse { - $data = json_decode($request->getContent(), true); - if (!is_array($data)) { - return $this->json(['error' => 'Ожидается JSON-объект в теле запроса'], Response::HTTP_BAD_REQUEST); - } - - $news = $this->newsCrud->update($news, $data); - - return $this->json($news, Response::HTTP_OK, [], [ - 'groups' => ['news:read'], - ]); + return $this->crud->update($request, $news, self::WRITE_GROUPS, self::READ_GROUPS); } #[IsGranted('ROLE_ADMIN')] #[Route('/{id}', name: 'news_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])] public function delete(News $news): JsonResponse { - $this->newsCrud->delete($news); - - return new JsonResponse(null, Response::HTTP_NO_CONTENT); + return $this->crud->delete($news); } } diff --git a/src/Controller/PromoController.php b/src/Controller/PromoController.php index dee5970..d2984e3 100644 --- a/src/Controller/PromoController.php +++ b/src/Controller/PromoController.php @@ -3,7 +3,10 @@ namespace App\Controller; use App\Entity\Promo; -use App\Service\PromoCrudService; +use App\Repository\PromoRepository; +use App\Service\Crud\CrudResponder; +use App\Service\Pagination\Paginator; +use OpenApi\Attributes as OA; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -14,72 +17,55 @@ use Symfony\Component\Security\Http\Attribute\IsGranted; #[Route('/promo')] final class PromoController extends AbstractController { + private const READ_GROUPS = ['promo:read']; + private const WRITE_GROUPS = ['promo:write']; + public function __construct( - private PromoCrudService $promoCrud, + private readonly CrudResponder $crud, + private readonly Paginator $paginator, ) { } + #[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: 'regionId', in: 'query', schema: new OA\Schema(type: 'integer'))] + #[OA\Parameter(name: 'active', in: 'query', schema: new OA\Schema(type: 'boolean'))] + #[OA\Parameter(name: 'search', in: 'query', schema: new OA\Schema(type: 'string'))] #[Route('/list', name: 'promo_list', methods: ['GET'])] - public function list(Request $request): JsonResponse + public function list(Request $request, PromoRepository $repository): JsonResponse { - $regionId = $request->query->getInt('regionId', 0); - $activeParam = $request->query->get('active'); - $active = $activeParam === null ? true : filter_var($activeParam, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); - if ($activeParam !== null && $active === null) { - return $this->json(['error' => 'Параметр active должен быть boolean'], Response::HTTP_BAD_REQUEST); - } + $qb = $repository->createFilteredQueryBuilder($request->query->all()); - return $this->json(['data' => $this->promoCrud->getList($regionId > 0 ? $regionId : null, $active)], Response::HTTP_OK, [], [ - 'groups' => ['promo:read'], + return $this->json($this->paginator->paginate($qb, $request), Response::HTTP_OK, [], [ + 'groups' => self::READ_GROUPS, ]); } #[Route('/{id}', name: 'promo_show', methods: ['GET'], requirements: ['id' => '\d+'])] public function show(Promo $promo): JsonResponse { - return $this->json($promo, Response::HTTP_OK, [], [ - 'groups' => ['promo:read'], - ]); + return $this->crud->read($promo, self::READ_GROUPS); } #[IsGranted('ROLE_ADMIN')] #[Route('/create', name: 'promo_create', methods: ['POST'])] public function create(Request $request): JsonResponse { - $data = json_decode($request->getContent(), true); - if (!is_array($data)) { - return $this->json(['error' => 'Ожидается JSON-объект в теле запроса'], Response::HTTP_BAD_REQUEST); - } - - $promo = $this->promoCrud->create($data); - - return $this->json($promo, Response::HTTP_CREATED, [], [ - 'groups' => ['promo:read'], - ]); + return $this->crud->create($request, Promo::class, self::WRITE_GROUPS, self::READ_GROUPS); } #[IsGranted('ROLE_ADMIN')] #[Route('/{id}', name: 'promo_update', methods: ['PUT'], requirements: ['id' => '\d+'])] - public function update(Promo $promo, Request $request): JsonResponse + public function update(Request $request, Promo $promo): JsonResponse { - $data = json_decode($request->getContent(), true); - if (!is_array($data)) { - return $this->json(['error' => 'Ожидается JSON-объект в теле запроса'], Response::HTTP_BAD_REQUEST); - } - - $promo = $this->promoCrud->update($promo, $data); - - return $this->json($promo, Response::HTTP_OK, [], [ - 'groups' => ['promo:read'], - ]); + return $this->crud->update($request, $promo, self::WRITE_GROUPS, self::READ_GROUPS); } #[IsGranted('ROLE_ADMIN')] #[Route('/{id}', name: 'promo_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])] public function delete(Promo $promo): JsonResponse { - $this->promoCrud->delete($promo); - - return new JsonResponse(null, Response::HTTP_NO_CONTENT); + return $this->crud->delete($promo); } } diff --git a/src/Controller/SiteServiceController.php b/src/Controller/SiteServiceController.php index f078bad..92c517d 100644 --- a/src/Controller/SiteServiceController.php +++ b/src/Controller/SiteServiceController.php @@ -3,7 +3,10 @@ namespace App\Controller; use App\Entity\SiteService; -use App\Service\SiteServiceCrudService; +use App\Repository\SiteServiceRepository; +use App\Service\Crud\CrudResponder; +use App\Service\Pagination\Paginator; +use OpenApi\Attributes as OA; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -14,91 +17,55 @@ use Symfony\Component\Security\Http\Attribute\IsGranted; #[Route('/site-services')] final class SiteServiceController extends AbstractController { + private const READ_GROUPS = ['site_service:read']; + private const WRITE_GROUPS = ['site_service:write']; + public function __construct( - private SiteServiceCrudService $siteServiceCrud, + private readonly CrudResponder $crud, + private readonly Paginator $paginator, ) { } + #[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: 'regionId', in: 'query', schema: new OA\Schema(type: 'integer'))] + #[OA\Parameter(name: 'active', in: 'query', schema: new OA\Schema(type: 'boolean'))] + #[OA\Parameter(name: 'search', in: 'query', schema: new OA\Schema(type: 'string'))] #[Route('/list', name: 'site_service_list', methods: ['GET'])] - public function list(Request $request): JsonResponse + public function list(Request $request, SiteServiceRepository $repository): JsonResponse { - $page = $request->query->getInt('page', 1); - $perPage = min($request->query->getInt('perPage', 50), 500); - $regionId = $request->query->getInt('regionId', 0) ?: null; - $activeParam = $request->query->get('active'); - $active = $activeParam === null ? true : filter_var($activeParam, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); - if ($activeParam !== null && $active === null) { - return $this->json(['error' => 'Параметр active должен быть boolean'], Response::HTTP_BAD_REQUEST); - } + $qb = $repository->createFilteredQueryBuilder($request->query->all()); - $result = $this->siteServiceCrud->getPaginatedList($page, $perPage, $regionId, $active); - $data = $result['data']; - $total = $result['total']; - $perPage = $result['per_page']; - $totalPages = (int) ceil($total / $perPage); - - return $this->json([ - 'data' => $data, - 'pagination' => [ - 'total' => $total, - 'count' => count($data), - 'per_page' => $perPage, - 'current_page' => $result['page'], - 'total_pages' => $totalPages, - 'has_previous_page' => $result['page'] > 1, - 'has_next_page' => $result['page'] < $totalPages, - ], - ], Response::HTTP_OK, [], [ - 'groups' => ['site_service:read'], + return $this->json($this->paginator->paginate($qb, $request), Response::HTTP_OK, [], [ + 'groups' => self::READ_GROUPS, ]); } #[Route('/{id}', name: 'site_service_show', methods: ['GET'], requirements: ['id' => '\d+'])] public function show(SiteService $siteService): JsonResponse { - return $this->json($siteService, Response::HTTP_OK, [], [ - 'groups' => ['site_service:read'], - ]); + return $this->crud->read($siteService, self::READ_GROUPS); } #[IsGranted('ROLE_ADMIN')] #[Route('/create', name: 'site_service_create', methods: ['POST'])] public function create(Request $request): JsonResponse { - $data = json_decode($request->getContent(), true); - if (!is_array($data)) { - return $this->json(['error' => 'Ожидается JSON-объект в теле запроса'], Response::HTTP_BAD_REQUEST); - } - - $siteService = $this->siteServiceCrud->create($data); - - return $this->json($siteService, Response::HTTP_CREATED, [], [ - 'groups' => ['site_service:read'], - ]); + return $this->crud->create($request, SiteService::class, self::WRITE_GROUPS, self::READ_GROUPS); } #[IsGranted('ROLE_ADMIN')] #[Route('/{id}', name: 'site_service_update', methods: ['PUT'], requirements: ['id' => '\d+'])] - public function update(SiteService $siteService, Request $request): JsonResponse + public function update(Request $request, SiteService $siteService): JsonResponse { - $data = json_decode($request->getContent(), true); - if (!is_array($data)) { - return $this->json(['error' => 'Ожидается JSON-объект в теле запроса'], Response::HTTP_BAD_REQUEST); - } - - $siteService = $this->siteServiceCrud->update($siteService, $data); - - return $this->json($siteService, Response::HTTP_OK, [], [ - 'groups' => ['site_service:read'], - ]); + return $this->crud->update($request, $siteService, self::WRITE_GROUPS, self::READ_GROUPS); } #[IsGranted('ROLE_ADMIN')] #[Route('/{id}', name: 'site_service_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])] public function delete(SiteService $siteService): JsonResponse { - $this->siteServiceCrud->delete($siteService); - - return new JsonResponse(null, Response::HTTP_NO_CONTENT); + return $this->crud->delete($siteService); } } diff --git a/src/Repository/ArticleRepository.php b/src/Repository/ArticleRepository.php index 697d852..7a63323 100644 --- a/src/Repository/ArticleRepository.php +++ b/src/Repository/ArticleRepository.php @@ -4,6 +4,7 @@ namespace App\Repository; use App\Entity\Article; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; /** @@ -16,58 +17,28 @@ class ArticleRepository extends ServiceEntityRepository parent::__construct($registry, Article::class); } - public function findByFilters(array $filters, int $page = 1, int $limit = 20): array + /** + * @param array $filters + */ + public function createFilteredQueryBuilder(array $filters): QueryBuilder { - $qb = $this->createQueryBuilder('a'); + $qb = $this->createQueryBuilder('a')->orderBy('a.id', 'DESC'); - if (isset($filters['alias']) && $filters['alias'] !== '') { - $qb->andWhere('a.alias = :alias') - ->setParameter('alias', $filters['alias']); - } - if (isset($filters['active']) && $filters['active'] !== '') { - $qb->andWhere('a.active = :active') - ->setParameter('active', filter_var($filters['active'], FILTER_VALIDATE_BOOLEAN)); - } - if (isset($filters['regionId']) && $filters['regionId'] !== '') { - $qb->andWhere('a.regionId = :regionId') - ->setParameter('regionId', (int) $filters['regionId']); - } + ContentRepositoryFilter::applyCommon($qb, 'a', $filters); - $qb->orderBy('a.id', 'DESC'); - - $qb->setFirstResult(($page - 1) * $limit) - ->setMaxResults($limit); - - return $qb->getQuery()->getResult(); - } - - public function countByFilters(array $filters): int - { - $qb = $this->createQueryBuilder('a') - ->select('COUNT(a.id)'); - - if (isset($filters['alias']) && $filters['alias'] !== '') { - $qb->andWhere('a.alias = :alias') - ->setParameter('alias', $filters['alias']); - } - if (isset($filters['active']) && $filters['active'] !== '') { - $qb->andWhere('a.active = :active') - ->setParameter('active', filter_var($filters['active'], FILTER_VALIDATE_BOOLEAN)); - } - if (isset($filters['regionId']) && $filters['regionId'] !== '') { - $qb->andWhere('a.regionId = :regionId') - ->setParameter('regionId', (int) $filters['regionId']); - } - - return (int) $qb->getQuery()->getSingleScalarResult(); + return $qb; } + /** + * Поиск статьи по alias с учётом возможных вариантов написания (исторический функционал). + */ public function findOneByAlias(string $alias): ?Article { $alias = trim($alias); if ($alias === '') { return null; } + $variants = [ $alias, $alias . '-', @@ -79,16 +50,18 @@ class ArticleRepository extends ServiceEntityRepository return $article; } } - // Поиск по TRIM(alias) в БД (нативный SQL для совместимости с PostgreSQL) + + // Фолбэк по TRIM(alias) в БД для совместимости со старыми данными. $conn = $this->getEntityManager()->getConnection(); $id = $conn->fetchOne( 'SELECT id FROM article WHERE TRIM(alias) = :alias LIMIT 1', ['alias' => $alias], - ['alias' => \PDO::PARAM_STR] + ['alias' => \PDO::PARAM_STR], ); if ($id !== false) { return $this->find($id); } + return null; } } diff --git a/src/Repository/ContentRepositoryFilter.php b/src/Repository/ContentRepositoryFilter.php new file mode 100644 index 0000000..d4235d8 --- /dev/null +++ b/src/Repository/ContentRepositoryFilter.php @@ -0,0 +1,123 @@ + 0 + * - active bool (по умолчанию true, если не передан) + * - alias строка (строгое равенство) + * - search LIKE по name (case-insensitive) + * + * Параметры пагинации (page/perPage) обрабатываются Paginator'ом и здесь игнорируются. + */ +final class ContentRepositoryFilter +{ + /** + * @param array $filters + */ + public static function applyCommon(QueryBuilder $qb, string $alias, array $filters): void + { + $regionId = self::extractInt($filters, ['regionId', 'region_id']); + if ($regionId !== null && $regionId > 0) { + $qb->andWhere("$alias.regionId = :regionId") + ->setParameter('regionId', $regionId); + } + + $active = self::extractBool($filters, ['active']); + if ($active !== null) { + $qb->andWhere("$alias.active = :active") + ->setParameter('active', $active); + } + + $aliasFilter = self::extractNonEmptyString($filters, ['alias']); + if ($aliasFilter !== null) { + $qb->andWhere("$alias.alias = :aliasValue") + ->setParameter('aliasValue', $aliasFilter); + } + + $search = self::extractNonEmptyString($filters, ['search', 'q']); + if ($search !== null) { + $qb->andWhere("LOWER($alias.name) LIKE :search") + ->setParameter('search', '%' . mb_strtolower($search) . '%'); + } + } + + /** + * @param array $filters + * @param list $keys + */ + private static function extractInt(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; + } + } + + return null; + } + + /** + * @param array $filters + * @param list $keys + */ + private static function extractBool(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; + } + + return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + } + + return null; + } + + /** + * @param array $filters + * @param list $keys + */ + private static function extractNonEmptyString(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; + } + } + + return null; + } +} diff --git a/src/Repository/DiseaseRepository.php b/src/Repository/DiseaseRepository.php index 24f4994..5e9eca7 100644 --- a/src/Repository/DiseaseRepository.php +++ b/src/Repository/DiseaseRepository.php @@ -4,6 +4,7 @@ namespace App\Repository; use App\Entity\Disease; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; /** @@ -18,4 +19,16 @@ class DiseaseRepository extends ServiceEntityRepository { parent::__construct($registry, Disease::class); } + + /** + * @param array $filters + */ + public function createFilteredQueryBuilder(array $filters): QueryBuilder + { + $qb = $this->createQueryBuilder('d')->orderBy('d.id', 'ASC'); + + ContentRepositoryFilter::applyCommon($qb, 'd', $filters); + + return $qb; + } } diff --git a/src/Repository/MedicalCenterRepository.php b/src/Repository/MedicalCenterRepository.php index 7088a7c..2b14f2a 100644 --- a/src/Repository/MedicalCenterRepository.php +++ b/src/Repository/MedicalCenterRepository.php @@ -4,6 +4,7 @@ namespace App\Repository; use App\Entity\MedicalCenter; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; /** @@ -18,4 +19,16 @@ class MedicalCenterRepository extends ServiceEntityRepository { parent::__construct($registry, MedicalCenter::class); } + + /** + * @param array $filters + */ + public function createFilteredQueryBuilder(array $filters): QueryBuilder + { + $qb = $this->createQueryBuilder('m')->orderBy('m.id', 'DESC'); + + ContentRepositoryFilter::applyCommon($qb, 'm', $filters); + + return $qb; + } } diff --git a/src/Repository/NewsRepository.php b/src/Repository/NewsRepository.php index 9607b31..7703f0d 100644 --- a/src/Repository/NewsRepository.php +++ b/src/Repository/NewsRepository.php @@ -4,6 +4,7 @@ namespace App\Repository; use App\Entity\News; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; /** @@ -18,4 +19,20 @@ class NewsRepository extends ServiceEntityRepository { parent::__construct($registry, News::class); } + + /** + * Готовит QueryBuilder под пагинацию (Pagerfanta\QueryAdapter). + * + * Поддерживаемые фильтры: regionId, active (по умолчанию true), alias, search. + * + * @param array $filters + */ + public function createFilteredQueryBuilder(array $filters): QueryBuilder + { + $qb = $this->createQueryBuilder('n')->orderBy('n.id', 'DESC'); + + ContentRepositoryFilter::applyCommon($qb, 'n', $filters); + + return $qb; + } } diff --git a/src/Repository/PromoRepository.php b/src/Repository/PromoRepository.php index 5a5c4c6..35e0b1e 100644 --- a/src/Repository/PromoRepository.php +++ b/src/Repository/PromoRepository.php @@ -4,6 +4,7 @@ namespace App\Repository; use App\Entity\Promo; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; /** @@ -18,4 +19,16 @@ class PromoRepository extends ServiceEntityRepository { parent::__construct($registry, Promo::class); } + + /** + * @param array $filters + */ + public function createFilteredQueryBuilder(array $filters): QueryBuilder + { + $qb = $this->createQueryBuilder('p')->orderBy('p.id', 'DESC'); + + ContentRepositoryFilter::applyCommon($qb, 'p', $filters); + + return $qb; + } } diff --git a/src/Repository/SiteServiceRepository.php b/src/Repository/SiteServiceRepository.php index 1a07399..c44abbd 100644 --- a/src/Repository/SiteServiceRepository.php +++ b/src/Repository/SiteServiceRepository.php @@ -4,6 +4,7 @@ namespace App\Repository; use App\Entity\SiteService; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; /** @@ -18,4 +19,16 @@ class SiteServiceRepository extends ServiceEntityRepository { parent::__construct($registry, SiteService::class); } + + /** + * @param array $filters + */ + public function createFilteredQueryBuilder(array $filters): QueryBuilder + { + $qb = $this->createQueryBuilder('s')->orderBy('s.id', 'ASC'); + + ContentRepositoryFilter::applyCommon($qb, 's', $filters); + + return $qb; + } } diff --git a/src/Service/Crud/CrudResponder.php b/src/Service/Crud/CrudResponder.php new file mode 100644 index 0000000..4e40251 --- /dev/null +++ b/src/Service/Crud/CrudResponder.php @@ -0,0 +1,184 @@ +crud->create($request, News::class, ['news:write'], ['news:read']); + */ +final class CrudResponder +{ + public function __construct( + private EntityManagerInterface $em, + private SerializerInterface $serializer, + private ValidatorInterface $validator, + ) { + } + + /** + * @param list $readGroups + */ + public function read(object $entity, array $readGroups): JsonResponse + { + return $this->json($entity, Response::HTTP_OK, $readGroups); + } + + /** + * @template T of object + * + * @param class-string $entityClass + * @param list $writeGroups + * @param list $readGroups + */ + public function create( + Request $request, + string $entityClass, + array $writeGroups, + array $readGroups, + bool $allowIdFromPayload = true, + ): JsonResponse { + $payload = $this->decodePayload($request); + if ($payload === null) { + return $this->jsonError('Ожидается JSON-объект в теле запроса', Response::HTTP_BAD_REQUEST); + } + + try { + /** @var T $entity */ + $entity = $this->serializer->deserialize( + $request->getContent(), + $entityClass, + 'json', + [ + AbstractNormalizer::GROUPS => $writeGroups, + ], + ); + } catch (SerializerExceptionInterface $e) { + return $this->jsonError('Ошибка десериализации: ' . $e->getMessage(), Response::HTTP_BAD_REQUEST); + } + + // Id в группе :write не присутствует (чтобы запретить инъекцию при update). + // На create поддерживаем явный id из payload, потому что у контент-сущностей + // нет GeneratedValue (id приходит из Bitrix-view). + if ($allowIdFromPayload && isset($payload['id']) && method_exists($entity, 'setId')) { + $id = (int) $payload['id']; + if ($id > 0) { + $entity->setId($id); + } + } + + if (($validationResponse = $this->validate($entity)) !== null) { + return $validationResponse; + } + + $this->em->persist($entity); + $this->em->flush(); + + return $this->json($entity, Response::HTTP_CREATED, $readGroups); + } + + /** + * @param list $writeGroups + * @param list $readGroups + */ + public function update( + Request $request, + object $entity, + array $writeGroups, + array $readGroups, + ): JsonResponse { + if ($this->decodePayload($request) === null) { + return $this->jsonError('Ожидается JSON-объект в теле запроса', Response::HTTP_BAD_REQUEST); + } + + try { + $this->serializer->deserialize( + $request->getContent(), + $entity::class, + 'json', + [ + AbstractNormalizer::GROUPS => $writeGroups, + AbstractNormalizer::OBJECT_TO_POPULATE => $entity, + ], + ); + } catch (SerializerExceptionInterface $e) { + return $this->jsonError('Ошибка десериализации: ' . $e->getMessage(), Response::HTTP_BAD_REQUEST); + } + + if (($validationResponse = $this->validate($entity)) !== null) { + return $validationResponse; + } + + $this->em->flush(); + + return $this->json($entity, Response::HTTP_OK, $readGroups); + } + + public function delete(object $entity): JsonResponse + { + $this->em->remove($entity); + $this->em->flush(); + + return new JsonResponse(null, Response::HTTP_NO_CONTENT); + } + + /** + * @return array|null null если тело не является JSON-объектом + */ + private function decodePayload(Request $request): ?array + { + $data = json_decode($request->getContent(), true); + + return is_array($data) ? $data : null; + } + + private function validate(object $entity): ?JsonResponse + { + $errors = $this->validator->validate($entity); + if (count($errors) === 0) { + return null; + } + + $formatted = []; + foreach ($errors as $error) { + $formatted[] = [ + 'property' => $error->getPropertyPath(), + 'message' => $error->getMessage(), + ]; + } + + return new JsonResponse(['errors' => $formatted], Response::HTTP_UNPROCESSABLE_ENTITY); + } + + /** + * @param list $groups + */ + private function json(mixed $data, int $status, array $groups): JsonResponse + { + $json = $this->serializer->serialize($data, 'json', [ + AbstractNormalizer::GROUPS => $groups, + ]); + + return new JsonResponse($json, $status, [], true); + } + + private function jsonError(string $message, int $status): JsonResponse + { + return new JsonResponse(['error' => $message], $status); + } +} diff --git a/src/Service/DiseaseCrudService.php b/src/Service/DiseaseCrudService.php index 01a0de1..ff42797 100644 --- a/src/Service/DiseaseCrudService.php +++ b/src/Service/DiseaseCrudService.php @@ -2,206 +2,26 @@ namespace App\Service; -use App\Entity\Disease; -use App\Repository\DiseaseRepository; use Doctrine\ORM\EntityManagerInterface; +/** + * Импорт заболеваний из материализованного представления (Bitrix view). + * + * См. DiseaseController + CrudResponder для CRUD; этот сервис — только syncFromView*. + */ final class DiseaseCrudService { public function __construct( private EntityManagerInterface $em, - private DiseaseRepository $diseaseRepository, ) { } - /** - * @return array{data: Disease[], total: int, page: int, per_page: int} - */ - public function getPaginatedList(int $page, int $perPage, ?int $regionId = null): array - { - $page = max(1, $page); - $perPage = min(max(1, $perPage), 500); - - $qb = $this->diseaseRepository->createQueryBuilder('d') - ->orderBy('d.id', 'ASC'); - - if ($regionId !== null) { - $qb->andWhere('d.regionId = :regionId') - ->setParameter('regionId', $regionId); - } - - $countQb = $this->diseaseRepository->createQueryBuilder('d') - ->select('COUNT(d.id)'); - if ($regionId !== null) { - $countQb->andWhere('d.regionId = :regionId') - ->setParameter('regionId', $regionId); - } - $total = (int) $countQb->getQuery()->getSingleScalarResult(); - - $qb->setFirstResult(($page - 1) * $perPage) - ->setMaxResults($perPage); - - $data = $qb->getQuery()->getResult(); - - return [ - 'data' => $data, - 'total' => $total, - 'page' => $page, - 'per_page' => $perPage, - ]; - } - - public function getShow(int $id): ?Disease - { - return $this->diseaseRepository->find($id); - } - - public function create(array $data): Disease - { - if (!array_key_exists('id', $data) || $data['id'] === null || $data['id'] === '') { - throw new \InvalidArgumentException('Поле id обязательно.'); - } - - $disease = new Disease(); - $this->updateEntity($disease, $data); - - $this->em->persist($disease); - $this->em->flush(); - - return $disease; - } - - public function update(Disease $disease, array $data): Disease - { - unset($data['id']); - $this->updateEntity($disease, $data); - - $this->em->flush(); - - return $disease; - } - - public function delete(Disease $disease): void - { - $this->em->remove($disease); - $this->em->flush(); - } - - private function updateEntity(Disease $disease, array $data): void - { - if (array_key_exists('id', $data) && $data['id'] !== null && $data['id'] !== '') { - $disease->setId((int) $data['id']); - } - - if (array_key_exists('name', $data)) { - $disease->setName($data['name']); - } - - if (array_key_exists('previewPicture', $data) || array_key_exists('preview_picture', $data)) { - $disease->setPreviewPicture($data['previewPicture'] ?? $data['preview_picture']); - } - - if (array_key_exists('active', $data)) { - $disease->setActive($data['active']); - } - - if (array_key_exists('regionId', $data) || array_key_exists('region_id', $data)) { - $v = $data['regionId'] ?? $data['region_id']; - $disease->setRegionId($v === null || $v === '' ? null : (int) $v); - } - - if (array_key_exists('alias', $data)) { - $disease->setAlias($data['alias']); - } - - if (array_key_exists('anons', $data)) { - $disease->setAnons($data['anons']); - } - - if (array_key_exists('updateAt', $data) || array_key_exists('update_at', $data)) { - $raw = $data['updateAt'] ?? $data['update_at']; - if ($raw === null || $raw === '') { - $disease->setUpdateAt(null); - } elseif ($raw instanceof \DateTimeInterface) { - $disease->setUpdateAt($raw); - } elseif (is_string($raw)) { - $disease->setUpdateAt(new \DateTimeImmutable($raw)); - } - } - - if (array_key_exists('hidePicture', $data) || array_key_exists('hide_picture', $data)) { - $disease->setHidePicture($data['hidePicture'] ?? $data['hide_picture']); - } - - if (array_key_exists('readTime', $data) || array_key_exists('read_time', $data)) { - $disease->setReadTime($data['readTime'] ?? $data['read_time']); - } - - if (array_key_exists('diseasesName', $data) || array_key_exists('diseases_name', $data)) { - $disease->setDiseasesName($data['diseasesName'] ?? $data['diseases_name']); - } - - if (array_key_exists('tagsImportant', $data) || array_key_exists('tags_important', $data)) { - $disease->setTagsImportant($data['tagsImportant'] ?? $data['tags_important']); - } - - if (array_key_exists('tags', $data)) { - $disease->setTags($data['tags']); - } - - if (array_key_exists('diseasesOtherName', $data) || array_key_exists('diseases_other_name', $data)) { - $disease->setDiseasesOtherName($data['diseasesOtherName'] ?? $data['diseases_other_name']); - } - - if (array_key_exists('symptom', $data)) { - $disease->setSymptom($data['symptom']); - } - - if (array_key_exists('staff', $data)) { - $disease->setStaff($data['staff']); - } - - if (array_key_exists('linkServices', $data) || array_key_exists('link_services', $data)) { - $disease->setLinkServices($data['linkServices'] ?? $data['link_services']); - } - - if (array_key_exists('staffList', $data) || array_key_exists('staff_list', $data)) { - $disease->setStaffList($data['staffList'] ?? $data['staff_list']); - } - - if (array_key_exists('staffPost', $data) || array_key_exists('staff_post', $data)) { - $disease->setStaffPost($data['staffPost'] ?? $data['staff_post']); - } - - if (array_key_exists('staffPostExclude', $data) || array_key_exists('staff_post_exclude', $data)) { - $disease->setStaffPostExclude($data['staffPostExclude'] ?? $data['staff_post_exclude']); - } - - if (array_key_exists('linkFaq', $data) || array_key_exists('link_faq', $data)) { - $disease->setLinkFaq($data['linkFaq'] ?? $data['link_faq']); - } - - if (array_key_exists('bibliography', $data)) { - $disease->setBibliography($data['bibliography']); - } - - if (array_key_exists('staffCheck', $data) || array_key_exists('staff_check', $data)) { - $disease->setStaffCheck($data['staffCheck'] ?? $data['staff_check']); - } - - if (array_key_exists('content', $data)) { - $disease->setContent($data['content']); - } - } - public function syncFromViewDisease(string $viewName = 'public.view_disease'): int { if (!preg_match('/^[A-Za-z0-9_\.]+$/', $viewName)) { throw new \InvalidArgumentException('Invalid view name'); } - $connection = $this->em->getConnection(); - $sql = sprintf( 'INSERT INTO disease ( id, @@ -282,6 +102,6 @@ final class DiseaseCrudService $viewName ); - return (int) $connection->executeStatement($sql); + return (int) $this->em->getConnection()->executeStatement($sql); } } diff --git a/src/Service/MedicalCenterCrudService.php b/src/Service/MedicalCenterCrudService.php index f5d6ed9..1dc1be7 100644 --- a/src/Service/MedicalCenterCrudService.php +++ b/src/Service/MedicalCenterCrudService.php @@ -2,312 +2,127 @@ namespace App\Service; -use App\Entity\MedicalCenter; -use App\Repository\MedicalCenterRepository; use Doctrine\ORM\EntityManagerInterface; +/** + * Импорт центров из материализованного представления (Bitrix view). + * + * См. MedicalCenterController + CrudResponder для CRUD; этот сервис — только syncFromView*. + */ final class MedicalCenterCrudService { - public function __construct( - private EntityManagerInterface $em, - private MedicalCenterRepository $medicalCenterRepository - ) { - } + public function __construct( + private EntityManagerInterface $em, + ) { + } - /** - * @return MedicalCenter[] - */ - public function getList(?int $regionId = null, ?bool $active = true): array - { - $criteria = []; - if ($regionId !== null) { - $criteria['regionId'] = $regionId; - } - if ($active !== null) { - $criteria['active'] = $active; - } + public function syncFromViewCenters(string $viewName = 'public.view_centers'): int + { + if (!preg_match('/^[A-Za-z0-9_\.]+$/', $viewName)) { + throw new \InvalidArgumentException('Invalid view name'); + } - return $this->medicalCenterRepository->findBy($criteria, ['id' => 'ASC']); - } + $sql = sprintf( + 'INSERT INTO medical_center ( + id, + name, + active, + region_id, + alias, + anons, + content, + update_at, + kod_uslug, + doctors, + services, + articles, + txt_up, + main_link_staff, + contraindications, + hide_picture, + indications, + link_sale, + plus_list, + plus_text, + plus_title, + process_text, + process_title, + services_list, + services_photos, + services_title, + sort_staff, + training_text, + training_text_title, + why_text, + why_title + ) + SELECT + id, + name, + active, + region_id, + alias, + anons, + content, + update_at, + kod_uslug, + doctors, + services, + articles, + txt_up, + main_link_staff, + contraindications, + hide_picture, + indications, + link_sale, + plus_list, + plus_text, + plus_title, + process_text, + process_title, + services_list, + services_photos, + services_title, + sort_staff, + training_text, + training_text_title, + why_text, + why_title + FROM %s + ON CONFLICT (id) DO UPDATE SET + name = EXCLUDED.name, + active = EXCLUDED.active, + region_id = EXCLUDED.region_id, + alias = EXCLUDED.alias, + anons = EXCLUDED.anons, + content = EXCLUDED.content, + update_at = EXCLUDED.update_at, + kod_uslug = EXCLUDED.kod_uslug, + doctors = EXCLUDED.doctors, + services = EXCLUDED.services, + articles = EXCLUDED.articles, + txt_up = EXCLUDED.txt_up, + main_link_staff = EXCLUDED.main_link_staff, + contraindications = EXCLUDED.contraindications, + hide_picture = EXCLUDED.hide_picture, + indications = EXCLUDED.indications, + link_sale = EXCLUDED.link_sale, + plus_list = EXCLUDED.plus_list, + plus_text = EXCLUDED.plus_text, + plus_title = EXCLUDED.plus_title, + process_text = EXCLUDED.process_text, + process_title = EXCLUDED.process_title, + services_list = EXCLUDED.services_list, + services_photos = EXCLUDED.services_photos, + services_title = EXCLUDED.services_title, + sort_staff = EXCLUDED.sort_staff, + training_text = EXCLUDED.training_text, + training_text_title = EXCLUDED.training_text_title, + why_text = EXCLUDED.why_text, + why_title = EXCLUDED.why_title', + $viewName + ); - public function getShow(int $id): ?MedicalCenter - { - return $this->medicalCenterRepository->find($id); - } - - public function create(array $data): MedicalCenter - { - $medicalCenter = new MedicalCenter(); - $this->updateEntity($medicalCenter, $data); - - $this->em->persist($medicalCenter); - $this->em->flush(); - - return $medicalCenter; - } - - public function update(MedicalCenter $medicalCenter, array $data): MedicalCenter - { - unset($data['id']); - $this->updateEntity($medicalCenter, $data); - - $this->em->flush(); - return $medicalCenter; - } - - public function delete(MedicalCenter $medicalCenter): void - { - $this->em->remove($medicalCenter); - $this->em->flush(); - } - - private function updateEntity(MedicalCenter $medicalCenter, array $data): void - { - if (array_key_exists('id', $data) && $data['id'] !== null && $data['id'] !== '') { - $medicalCenter->setId((int) $data['id']); - } - - if (array_key_exists('name', $data)) { - $medicalCenter->setName($data['name']); - } - - if (array_key_exists('active', $data)) { - $medicalCenter->setActive($data['active']); - } - - if (array_key_exists('regionId', $data) || array_key_exists('region_id', $data)) { - $v = $data['regionId'] ?? $data['region_id']; - $medicalCenter->setRegionId($v === null || $v === '' ? null : (int) $v); - } - - if (array_key_exists('alias', $data)) { - $medicalCenter->setAlias($data['alias']); - } - - if (array_key_exists('anons', $data)) { - $medicalCenter->setAnons($data['anons']); - } - - if (array_key_exists('content', $data)) { - $medicalCenter->setContent($data['content']); - } - - if (array_key_exists('updateAt', $data) || array_key_exists('update_at', $data)) { - $raw = $data['updateAt'] ?? $data['update_at']; - if ($raw === null || $raw === '') { - $medicalCenter->setUpdateAt(null); - } elseif ($raw instanceof \DateTimeInterface) { - $medicalCenter->setUpdateAt($raw); - } elseif (is_string($raw)) { - $medicalCenter->setUpdateAt(new \DateTimeImmutable($raw)); - } - } - - if (array_key_exists('kodUslug', $data) || array_key_exists('kod_uslug', $data)) { - $medicalCenter->setKodUslug($data['kodUslug'] ?? $data['kod_uslug']); - } - - if (array_key_exists('doctors', $data)) { - $medicalCenter->setDoctors($data['doctors']); - } - - if (array_key_exists('services', $data)) { - $medicalCenter->setServices($data['services']); - } - - if (array_key_exists('articles', $data)) { - $medicalCenter->setArticles($data['articles']); - } - - if (array_key_exists('txtUp', $data) || array_key_exists('txt_up', $data)) { - $medicalCenter->setTxtUp($data['txtUp'] ?? $data['txt_up']); - } - - if (array_key_exists('mainLinkStaff', $data) || array_key_exists('main_link_staff', $data)) { - $medicalCenter->setMainLinkStaff($data['mainLinkStaff'] ?? $data['main_link_staff']); - } - - if (array_key_exists('contraindications', $data)) { - $medicalCenter->setContraindications($data['contraindications']); - } - - if (array_key_exists('hidePicture', $data) || array_key_exists('hide_picture', $data)) { - $v = $data['hidePicture'] ?? $data['hide_picture']; - $medicalCenter->setHidePicture($v === null || $v === '' ? null : (int) $v); - } - - if (array_key_exists('indications', $data)) { - $medicalCenter->setIndications($data['indications']); - } - - if (array_key_exists('linkSale', $data) || array_key_exists('link_sale', $data)) { - $medicalCenter->setLinkSale($data['linkSale'] ?? $data['link_sale']); - } - - if (array_key_exists('plusList', $data) || array_key_exists('plus_list', $data)) { - $medicalCenter->setPlusList($data['plusList'] ?? $data['plus_list']); - } - - if (array_key_exists('plusText', $data) || array_key_exists('plus_text', $data)) { - $medicalCenter->setPlusText($data['plusText'] ?? $data['plus_text']); - } - - if (array_key_exists('plusTitle', $data) || array_key_exists('plus_title', $data)) { - $medicalCenter->setPlusTitle($data['plusTitle'] ?? $data['plus_title']); - } - - if (array_key_exists('processText', $data) || array_key_exists('process_text', $data)) { - $medicalCenter->setProcessText($data['processText'] ?? $data['process_text']); - } - - if (array_key_exists('processTitle', $data) || array_key_exists('process_title', $data)) { - $medicalCenter->setProcessTitle($data['processTitle'] ?? $data['process_title']); - } - - if (array_key_exists('servicesList', $data) || array_key_exists('services_list', $data)) { - $medicalCenter->setServicesList($data['servicesList'] ?? $data['services_list']); - } - - if (array_key_exists('servicesPhotos', $data) || array_key_exists('services_photos', $data)) { - $medicalCenter->setServicesPhotos($data['servicesPhotos'] ?? $data['services_photos']); - } - - if (array_key_exists('servicesTitle', $data) || array_key_exists('services_title', $data)) { - $medicalCenter->setServicesTitle($data['servicesTitle'] ?? $data['services_title']); - } - - if (array_key_exists('sortStaff', $data) || array_key_exists('sort_staff', $data)) { - $medicalCenter->setSortStaff($data['sortStaff'] ?? $data['sort_staff']); - } - - if (array_key_exists('trainingText', $data) || array_key_exists('training_text', $data)) { - $medicalCenter->setTrainingText($data['trainingText'] ?? $data['training_text']); - } - - if (array_key_exists('trainingTextTitle', $data) || array_key_exists('training_text_title', $data)) { - $medicalCenter->setTrainingTextTitle($data['trainingTextTitle'] ?? $data['training_text_title']); - } - - if (array_key_exists('whyText', $data) || array_key_exists('why_text', $data)) { - $medicalCenter->setWhyText($data['whyText'] ?? $data['why_text']); - } - - if (array_key_exists('whyTitle', $data) || array_key_exists('why_title', $data)) { - $medicalCenter->setWhyTitle($data['whyTitle'] ?? $data['why_title']); - } - } - - public function syncFromViewCenters(string $viewName = 'public.view_centers'): int - { - // В опции разрешаем только идентификаторы (буквы/цифры/underscore) и точку для схемы. - if (!preg_match('/^[A-Za-z0-9_\.]+$/', $viewName)) { - throw new \InvalidArgumentException('Invalid view name'); - } - - $connection = $this->em->getConnection(); - - $sql = sprintf( - 'INSERT INTO medical_center ( - id, - name, - active, - region_id, - alias, - anons, - content, - update_at, - kod_uslug, - doctors, - services, - articles, - txt_up, - main_link_staff, - contraindications, - hide_picture, - indications, - link_sale, - plus_list, - plus_text, - plus_title, - process_text, - process_title, - services_list, - services_photos, - services_title, - sort_staff, - training_text, - training_text_title, - why_text, - why_title - ) - SELECT - id, - name, - active, - region_id, - alias, - anons, - content, - update_at, - kod_uslug, - doctors, - services, - articles, - txt_up, - main_link_staff, - contraindications, - hide_picture, - indications, - link_sale, - plus_list, - plus_text, - plus_title, - process_text, - process_title, - services_list, - services_photos, - services_title, - sort_staff, - training_text, - training_text_title, - why_text, - why_title - FROM %s - ON CONFLICT (id) DO UPDATE SET - name = EXCLUDED.name, - active = EXCLUDED.active, - region_id = EXCLUDED.region_id, - alias = EXCLUDED.alias, - anons = EXCLUDED.anons, - content = EXCLUDED.content, - update_at = EXCLUDED.update_at, - kod_uslug = EXCLUDED.kod_uslug, - doctors = EXCLUDED.doctors, - services = EXCLUDED.services, - articles = EXCLUDED.articles, - txt_up = EXCLUDED.txt_up, - main_link_staff = EXCLUDED.main_link_staff, - contraindications = EXCLUDED.contraindications, - hide_picture = EXCLUDED.hide_picture, - indications = EXCLUDED.indications, - link_sale = EXCLUDED.link_sale, - plus_list = EXCLUDED.plus_list, - plus_text = EXCLUDED.plus_text, - plus_title = EXCLUDED.plus_title, - process_text = EXCLUDED.process_text, - process_title = EXCLUDED.process_title, - services_list = EXCLUDED.services_list, - services_photos = EXCLUDED.services_photos, - services_title = EXCLUDED.services_title, - sort_staff = EXCLUDED.sort_staff, - training_text = EXCLUDED.training_text, - training_text_title = EXCLUDED.training_text_title, - why_text = EXCLUDED.why_text, - why_title = EXCLUDED.why_title', - $viewName - ); - - return (int) $connection->executeStatement($sql); - } + return (int) $this->em->getConnection()->executeStatement($sql); + } } - diff --git a/src/Service/NewsCrudService.php b/src/Service/NewsCrudService.php index 988f1ed..c5a7fd0 100644 --- a/src/Service/NewsCrudService.php +++ b/src/Service/NewsCrudService.php @@ -2,148 +2,28 @@ namespace App\Service; -use App\Entity\News; -use App\Repository\NewsRepository; use Doctrine\ORM\EntityManagerInterface; +/** + * Импорт новостей из материализованного представления (Bitrix view). + * + * CRUD (create/update/delete/list) живёт теперь в NewsController через + * общие App\Service\Crud\CrudResponder и App\Service\Pagination\Paginator — + * этот сервис отвечает только за синхронизацию (см. App\Command\UploadNewsCommand). + */ final class NewsCrudService { public function __construct( private EntityManagerInterface $em, - private NewsRepository $newsRepository ) { } - /** - * @return News[] - */ - public function getList(?int $regionId = null, ?bool $active = true): array - { - $criteria = []; - if ($regionId !== null) { - $criteria['regionId'] = $regionId; - } - if ($active !== null) { - $criteria['active'] = $active; - } - - return $this->newsRepository->findBy($criteria, ['id' => 'ASC']); - } - - public function getShow(int $id): ?News - { - return $this->newsRepository->find($id); - } - - public function create(array $data): News - { - $news = new News(); - $this->updateEntity($news, $data); - - $this->em->persist($news); - $this->em->flush(); - - return $news; - } - - public function update(News $news, array $data): News - { - unset($data['id']); - $this->updateEntity($news, $data); - - $this->em->flush(); - return $news; - } - - public function delete(News $news): void - { - $this->em->remove($news); - $this->em->flush(); - } - - private function updateEntity(News $news, array $data): void - { - if (array_key_exists('id', $data) && $data['id'] !== null && $data['id'] !== '') { - $news->setId((int) $data['id']); - } - - if (array_key_exists('name', $data)) { - $news->setName($data['name']); - } - - if (array_key_exists('active', $data)) { - $news->setActive($data['active']); - } - - if (array_key_exists('regionId', $data) || array_key_exists('region_id', $data)) { - $v = $data['regionId'] ?? $data['region_id']; - $news->setRegionId($v === null || $v === '' ? null : (int) $v); - } - - if (array_key_exists('alias', $data)) { - $news->setAlias($data['alias']); - } - - if (array_key_exists('anons', $data)) { - $news->setAnons($data['anons']); - } - - if (array_key_exists('content', $data)) { - $news->setContent($data['content']); - } - - if (array_key_exists('updateAt', $data) || array_key_exists('update_at', $data)) { - $raw = $data['updateAt'] ?? $data['update_at']; - if ($raw === null || $raw === '') { - $news->setUpdateAt(null); - } elseif ($raw instanceof \DateTimeInterface) { - $news->setUpdateAt($raw); - } elseif (is_string($raw)) { - $news->setUpdateAt(new \DateTimeImmutable($raw)); - } - } - - if (array_key_exists('linkElPrice', $data) || array_key_exists('link_el_price', $data)) { - $news->setLinkElPrice($data['linkElPrice'] ?? $data['link_el_price']); - } - - if (array_key_exists('shortName', $data) || array_key_exists('short_name', $data)) { - $news->setShortName($data['shortName'] ?? $data['short_name']); - } - - if (array_key_exists('timer', $data)) { - $news->setTimer($data['timer']); - } - - if (array_key_exists('timerBg', $data) || array_key_exists('timer_bg', $data)) { - $news->setTimerBg($data['timerBg'] ?? $data['timer_bg']); - } - - if (array_key_exists('formOrder', $data) || array_key_exists('form_order', $data)) { - $news->setFormOrder($data['formOrder'] ?? $data['form_order']); - } - - if (array_key_exists('linkServices', $data) || array_key_exists('link_services', $data)) { - $news->setLinkServices($data['linkServices'] ?? $data['link_services']); - } - - if (array_key_exists('linkStaff', $data) || array_key_exists('link_staff', $data)) { - $news->setLinkStaff($data['linkStaff'] ?? $data['link_staff']); - } - - if (array_key_exists('photos', $data)) { - $news->setPhotos($data['photos']); - } - } - public function syncFromViewNews(string $viewName = 'public.view_news'): int { if (!preg_match('/^[A-Za-z0-9_\.]+$/', $viewName)) { throw new \InvalidArgumentException('Invalid view name'); } - $connection = $this->em->getConnection(); - $sql = sprintf( 'INSERT INTO news ( id, @@ -200,6 +80,6 @@ final class NewsCrudService $viewName ); - return (int) $connection->executeStatement($sql); + return (int) $this->em->getConnection()->executeStatement($sql); } } diff --git a/src/Service/Pagination/Paginator.php b/src/Service/Pagination/Paginator.php new file mode 100644 index 0000000..527908a --- /dev/null +++ b/src/Service/Pagination/Paginator.php @@ -0,0 +1,65 @@ + [...], 'pagination' => [...]] в едином формате для всех контроллеров. + */ +final class Paginator +{ + public const DEFAULT_PER_PAGE = 50; + public const MAX_PER_PAGE = 500; + + /** + * @return array{data: list, pagination: array} + */ + public function paginate( + QueryBuilder $qb, + Request $request, + int $defaultPerPage = self::DEFAULT_PER_PAGE, + int $maxPerPage = self::MAX_PER_PAGE, + ): array { + $page = max(1, $request->query->getInt('page', 1)); + $perPage = min( + max(1, $request->query->getInt('perPage', $defaultPerPage)), + $maxPerPage, + ); + + $pagerfanta = (new Pagerfanta(new QueryAdapter($qb))) + ->setMaxPerPage($perPage); + + try { + $pagerfanta->setCurrentPage($page); + } catch (NotValidCurrentPageException) { + // выходим за пределы — возвращаем пустую страницу с корректным total + $pagerfanta->setCurrentPage(max(1, $pagerfanta->getNbPages())); + } + + $data = iterator_to_array($pagerfanta->getCurrentPageResults(), false); + + return [ + 'data' => $data, + 'pagination' => [ + 'total' => $pagerfanta->getNbResults(), + 'count' => count($data), + 'per_page' => $pagerfanta->getMaxPerPage(), + 'current_page' => $pagerfanta->getCurrentPage(), + 'total_pages' => $pagerfanta->getNbPages(), + 'has_previous_page' => $pagerfanta->hasPreviousPage(), + 'has_next_page' => $pagerfanta->hasNextPage(), + ], + ]; + } +} diff --git a/src/Service/PromoCrudService.php b/src/Service/PromoCrudService.php index e0f2fab..c6b21b6 100644 --- a/src/Service/PromoCrudService.php +++ b/src/Service/PromoCrudService.php @@ -2,148 +2,26 @@ namespace App\Service; -use App\Entity\Promo; -use App\Repository\PromoRepository; use Doctrine\ORM\EntityManagerInterface; +/** + * Импорт акций из материализованного представления (Bitrix view). + * + * См. PromoController + CrudResponder для CRUD; этот сервис — только syncFromView*. + */ final class PromoCrudService { public function __construct( private EntityManagerInterface $em, - private PromoRepository $promoRepository ) { } - /** - * @return Promo[] - */ - public function getList(?int $regionId = null, ?bool $active = true): array - { - $criteria = []; - if ($regionId !== null) { - $criteria['regionId'] = $regionId; - } - if ($active !== null) { - $criteria['active'] = $active; - } - - return $this->promoRepository->findBy($criteria, ['id' => 'ASC']); - } - - public function getShow(int $id): ?Promo - { - return $this->promoRepository->find($id); - } - - public function create(array $data): Promo - { - $promo = new Promo(); - $this->updateEntity($promo, $data); - - $this->em->persist($promo); - $this->em->flush(); - - return $promo; - } - - public function update(Promo $promo, array $data): Promo - { - unset($data['id']); - $this->updateEntity($promo, $data); - - $this->em->flush(); - return $promo; - } - - public function delete(Promo $promo): void - { - $this->em->remove($promo); - $this->em->flush(); - } - - private function updateEntity(Promo $promo, array $data): void - { - if (array_key_exists('id', $data) && $data['id'] !== null && $data['id'] !== '') { - $promo->setId((int) $data['id']); - } - - if (array_key_exists('name', $data)) { - $promo->setName($data['name']); - } - - if (array_key_exists('active', $data)) { - $promo->setActive($data['active']); - } - - if (array_key_exists('regionId', $data) || array_key_exists('region_id', $data)) { - $v = $data['regionId'] ?? $data['region_id']; - $promo->setRegionId($v === null || $v === '' ? null : (int) $v); - } - - if (array_key_exists('alias', $data)) { - $promo->setAlias($data['alias']); - } - - if (array_key_exists('anons', $data)) { - $promo->setAnons($data['anons']); - } - - if (array_key_exists('content', $data)) { - $promo->setContent($data['content']); - } - - if (array_key_exists('updateAt', $data) || array_key_exists('update_at', $data)) { - $raw = $data['updateAt'] ?? $data['update_at']; - if ($raw === null || $raw === '') { - $promo->setUpdateAt(null); - } elseif ($raw instanceof \DateTimeInterface) { - $promo->setUpdateAt($raw); - } elseif (is_string($raw)) { - $promo->setUpdateAt(new \DateTimeImmutable($raw)); - } - } - - if (array_key_exists('clinics', $data)) { - $promo->setClinics($data['clinics']); - } - - if (array_key_exists('timer', $data)) { - $promo->setTimer($data['timer']); - } - - if (array_key_exists('timerBg', $data) || array_key_exists('timer_bg', $data)) { - $promo->setTimerBg($data['timerBg'] ?? $data['timer_bg']); - } - - if (array_key_exists('shortName', $data) || array_key_exists('short_name', $data)) { - $promo->setShortName($data['shortName'] ?? $data['short_name']); - } - - if (array_key_exists('linkServices', $data) || array_key_exists('link_services', $data)) { - $promo->setLinkServices($data['linkServices'] ?? $data['link_services']); - } - - if (array_key_exists('linkStaff', $data) || array_key_exists('link_staff', $data)) { - $promo->setLinkStaff($data['linkStaff'] ?? $data['link_staff']); - } - - if (array_key_exists('period', $data)) { - $promo->setPeriod($data['period']); - } - - if (array_key_exists('photos', $data)) { - $promo->setPhotos($data['photos']); - } - } - public function syncFromViewPromo(string $viewName = 'public.view_promo'): int { if (!preg_match('/^[A-Za-z0-9_\.]+$/', $viewName)) { throw new \InvalidArgumentException('Invalid view name'); } - $connection = $this->em->getConnection(); - $sql = sprintf( 'INSERT INTO promo ( id, @@ -200,6 +78,6 @@ final class PromoCrudService $viewName ); - return (int) $connection->executeStatement($sql); + return (int) $this->em->getConnection()->executeStatement($sql); } } diff --git a/src/Service/SiteServiceCrudService.php b/src/Service/SiteServiceCrudService.php index fc7b607..befae42 100644 --- a/src/Service/SiteServiceCrudService.php +++ b/src/Service/SiteServiceCrudService.php @@ -2,358 +2,26 @@ namespace App\Service; -use App\Entity\SiteService; -use App\Repository\SiteServiceRepository; use Doctrine\ORM\EntityManagerInterface; +/** + * Импорт услуг из материализованного представления (Bitrix view). + * + * См. SiteServiceController + CrudResponder для CRUD; этот сервис — только syncFromView*. + */ final class SiteServiceCrudService { public function __construct( private EntityManagerInterface $em, - private SiteServiceRepository $siteServiceRepository, ) { } - /** - * @return SiteService[] - */ - public function getList(?int $regionId = null, ?bool $active = true): array - { - $criteria = []; - if ($regionId !== null) { - $criteria['regionId'] = $regionId; - } - if ($active !== null) { - $criteria['active'] = $active; - } - - return $this->siteServiceRepository->findBy($criteria, ['id' => 'ASC']); - } - - /** - * @return array{data: SiteService[], total: int, page: int, per_page: int} - */ - public function getPaginatedList(int $page, int $perPage, ?int $regionId = null, ?bool $active = true): array - { - $page = max(1, $page); - $perPage = min(max(1, $perPage), 500); - - $countQb = $this->siteServiceRepository->createQueryBuilder('s') - ->select('COUNT(s.id)'); - if ($regionId !== null) { - $countQb->andWhere('s.regionId = :regionId') - ->setParameter('regionId', $regionId); - } - if ($active !== null) { - $countQb->andWhere('s.active = :active') - ->setParameter('active', $active); - } - $total = (int) $countQb->getQuery()->getSingleScalarResult(); - - $qb = $this->siteServiceRepository->createQueryBuilder('s') - ->orderBy('s.id', 'ASC'); - if ($regionId !== null) { - $qb->andWhere('s.regionId = :regionId') - ->setParameter('regionId', $regionId); - } - if ($active !== null) { - $qb->andWhere('s.active = :active') - ->setParameter('active', $active); - } - $qb->setFirstResult(($page - 1) * $perPage) - ->setMaxResults($perPage); - - $data = $qb->getQuery()->getResult(); - - return [ - 'data' => $data, - 'total' => $total, - 'page' => $page, - 'per_page' => $perPage, - ]; - } - - public function getShow(int $id): ?SiteService - { - return $this->siteServiceRepository->find($id); - } - - public function create(array $data): SiteService - { - $siteService = new SiteService(); - $this->updateEntity($siteService, $data); - - $this->em->persist($siteService); - $this->em->flush(); - - return $siteService; - } - - public function update(SiteService $siteService, array $data): SiteService - { - unset($data['id']); - $this->updateEntity($siteService, $data); - - $this->em->flush(); - - return $siteService; - } - - public function delete(SiteService $siteService): void - { - $this->em->remove($siteService); - $this->em->flush(); - } - - private function updateEntity(SiteService $siteService, array $data): void - { - if (array_key_exists('id', $data)) { - $v = $data['id']; - $siteService->setId($v === null || $v === '' ? null : (int) $v); - } - - if (array_key_exists('name', $data)) { - $siteService->setName($data['name']); - } - - if (array_key_exists('active', $data)) { - $siteService->setActive($data['active']); - } - - if (array_key_exists('regionId', $data) || array_key_exists('region_id', $data)) { - $v = $data['regionId'] ?? $data['region_id']; - $siteService->setRegionId($v === null || $v === '' ? null : (int) $v); - } - - if (array_key_exists('alias', $data)) { - $siteService->setAlias($data['alias']); - } - - if (array_key_exists('anons', $data)) { - $siteService->setAnons($data['anons']); - } - - if (array_key_exists('content', $data)) { - $siteService->setContent($data['content']); - } - - if (array_key_exists('updateAt', $data) || array_key_exists('update_at', $data)) { - $raw = $data['updateAt'] ?? $data['update_at']; - if ($raw === null || $raw === '') { - $siteService->setUpdateAt(null); - } elseif ($raw instanceof \DateTimeInterface) { - $siteService->setUpdateAt($raw); - } elseif (is_string($raw)) { - $siteService->setUpdateAt(new \DateTimeImmutable($raw)); - } - } - - if (array_key_exists('linkVideoreviews', $data) || array_key_exists('link_videoreviews', $data)) { - $siteService->setLinkVideoreviews($data['linkVideoreviews'] ?? $data['link_videoreviews']); - } - - if (array_key_exists('previewImg', $data) || array_key_exists('preview_img', $data)) { - $siteService->setPreviewImg($data['previewImg'] ?? $data['preview_img']); - } - - if (array_key_exists('faq', $data)) { - $siteService->setFaq($data['faq']); - } - - if (array_key_exists('partPrice', $data) || array_key_exists('part_price', $data)) { - $siteService->setPartPrice($data['partPrice'] ?? $data['part_price']); - } - - if (array_key_exists('pokazaniya', $data)) { - $siteService->setPokazaniya($data['pokazaniya']); - } - - if (array_key_exists('preparation', $data)) { - $siteService->setPreparation($data['preparation']); - } - - if (array_key_exists('protivopokazaniya', $data)) { - $siteService->setProtivopokazaniya($data['protivopokazaniya']); - } - - if (array_key_exists('hideSignBtn', $data) || array_key_exists('hide_sign_btn', $data)) { - $siteService->setHideSignBtn($data['hideSignBtn'] ?? $data['hide_sign_btn']); - } - - if (array_key_exists('quiz', $data)) { - $siteService->setQuiz($data['quiz']); - } - - if (array_key_exists('tags', $data)) { - $siteService->setTags($data['tags']); - } - - if (array_key_exists('tagsImportant', $data) || array_key_exists('tags_important', $data)) { - $siteService->setTagsImportant($data['tagsImportant'] ?? $data['tags_important']); - } - - if (array_key_exists('bannerImg', $data) || array_key_exists('banner_img', $data)) { - $siteService->setBannerImg($data['bannerImg'] ?? $data['banner_img']); - } - - if (array_key_exists('bannerImgM', $data) || array_key_exists('banner_img_m', $data)) { - $siteService->setBannerImgM($data['bannerImgM'] ?? $data['banner_img_m']); - } - - if (array_key_exists('bannerImgUrl', $data) || array_key_exists('banner_img_url', $data)) { - $siteService->setBannerImgUrl($data['bannerImgUrl'] ?? $data['banner_img_url']); - } - - if (array_key_exists('clinics', $data)) { - $siteService->setClinics($data['clinics']); - } - - if (array_key_exists('downloadFile', $data) || array_key_exists('download_file', $data)) { - $siteService->setDownloadFile($data['downloadFile'] ?? $data['download_file']); - } - - if (array_key_exists('fullWidthBanner', $data) || array_key_exists('full_width_banner', $data)) { - $siteService->setFullWidthBanner($data['fullWidthBanner'] ?? $data['full_width_banner']); - } - - if (array_key_exists('staffUp', $data) || array_key_exists('staff_up', $data)) { - $siteService->setStaffUp($data['staffUp'] ?? $data['staff_up']); - } - - if (array_key_exists('advantages', $data)) { - $siteService->setAdvantages($data['advantages']); - } - - if (array_key_exists('hidePicture', $data) || array_key_exists('hide_picture', $data)) { - $v = $data['hidePicture'] ?? $data['hide_picture']; - $siteService->setHidePicture($v === null || $v === '' ? null : (int) $v); - } - - if (array_key_exists('kodUslug', $data) || array_key_exists('kod_uslug', $data)) { - $siteService->setKodUslug($data['kodUslug'] ?? $data['kod_uslug']); - } - - if (array_key_exists('linkPrice', $data) || array_key_exists('link_price', $data)) { - $siteService->setLinkPrice($data['linkPrice'] ?? $data['link_price']); - } - - if (array_key_exists('photosTitle', $data) || array_key_exists('photos_title', $data)) { - $siteService->setPhotosTitle($data['photosTitle'] ?? $data['photos_title']); - } - - if (array_key_exists('saleId', $data) || array_key_exists('sale_id', $data)) { - $siteService->setSaleId($data['saleId'] ?? $data['sale_id']); - } - - if (array_key_exists('sortStaff', $data) || array_key_exists('sort_staff', $data)) { - $siteService->setSortStaff($data['sortStaff'] ?? $data['sort_staff']); - } - - if (array_key_exists('contraindicationsList', $data) || array_key_exists('contraindications_list', $data)) { - $siteService->setContraindicationsList($data['contraindicationsList'] ?? $data['contraindications_list']); - } - - if (array_key_exists('customBlockText', $data) || array_key_exists('custom_block_text', $data)) { - $siteService->setCustomBlockText($data['customBlockText'] ?? $data['custom_block_text']); - } - - if (array_key_exists('customBlockText2', $data) || array_key_exists('custom_block_text2', $data)) { - $siteService->setCustomBlockText2($data['customBlockText2'] ?? $data['custom_block_text2']); - } - - if (array_key_exists('customBlockTitle', $data) || array_key_exists('custom_block_title', $data)) { - $siteService->setCustomBlockTitle($data['customBlockTitle'] ?? $data['custom_block_title']); - } - - if (array_key_exists('customBlockTitle2', $data) || array_key_exists('custom_block_title2', $data)) { - $siteService->setCustomBlockTitle2($data['customBlockTitle2'] ?? $data['custom_block_title2']); - } - - if (array_key_exists('indicationsList', $data) || array_key_exists('indications_list', $data)) { - $siteService->setIndicationsList($data['indicationsList'] ?? $data['indications_list']); - } - - if (array_key_exists('linkArticlesServices', $data) || array_key_exists('link_articles_services', $data)) { - $siteService->setLinkArticlesServices($data['linkArticlesServices'] ?? $data['link_articles_services']); - } - - if (array_key_exists('plusList', $data) || array_key_exists('plus_list', $data)) { - $siteService->setPlusList($data['plusList'] ?? $data['plus_list']); - } - - if (array_key_exists('plusText', $data) || array_key_exists('plus_text', $data)) { - $siteService->setPlusText($data['plusText'] ?? $data['plus_text']); - } - - if (array_key_exists('plusTitle', $data) || array_key_exists('plus_title', $data)) { - $siteService->setPlusTitle($data['plusTitle'] ?? $data['plus_title']); - } - - if (array_key_exists('prepareTitle', $data) || array_key_exists('prepare_title', $data)) { - $siteService->setPrepareTitle($data['prepareTitle'] ?? $data['prepare_title']); - } - - if (array_key_exists('processText', $data) || array_key_exists('process_text', $data)) { - $siteService->setProcessText($data['processText'] ?? $data['process_text']); - } - - if (array_key_exists('processTitle', $data) || array_key_exists('process_title', $data)) { - $siteService->setProcessTitle($data['processTitle'] ?? $data['process_title']); - } - - if (array_key_exists('servicesList', $data) || array_key_exists('services_list', $data)) { - $siteService->setServicesList($data['servicesList'] ?? $data['services_list']); - } - - if (array_key_exists('servicesPhotos', $data) || array_key_exists('services_photos', $data)) { - $siteService->setServicesPhotos($data['servicesPhotos'] ?? $data['services_photos']); - } - - if (array_key_exists('servicesTitle', $data) || array_key_exists('services_title', $data)) { - $siteService->setServicesTitle($data['servicesTitle'] ?? $data['services_title']); - } - - if (array_key_exists('textUp', $data) || array_key_exists('text_up', $data)) { - $siteService->setTextUp($data['textUp'] ?? $data['text_up']); - } - - if (array_key_exists('trainingText', $data) || array_key_exists('training_text', $data)) { - $siteService->setTrainingText($data['trainingText'] ?? $data['training_text']); - } - - if (array_key_exists('whyText', $data) || array_key_exists('why_text', $data)) { - $siteService->setWhyText($data['whyText'] ?? $data['why_text']); - } - - if (array_key_exists('whyTitle', $data) || array_key_exists('why_title', $data)) { - $siteService->setWhyTitle($data['whyTitle'] ?? $data['why_title']); - } - - if (array_key_exists('linkFaq', $data) || array_key_exists('link_faq', $data)) { - $siteService->setLinkFaq($data['linkFaq'] ?? $data['link_faq']); - } - - if (array_key_exists('linkServices', $data) || array_key_exists('link_services', $data)) { - $siteService->setLinkServices($data['linkServices'] ?? $data['link_services']); - } - - if (array_key_exists('linkStaff', $data) || array_key_exists('link_staff', $data)) { - $siteService->setLinkStaff($data['linkStaff'] ?? $data['link_staff']); - } - - if (array_key_exists('photos', $data)) { - $siteService->setPhotos($data['photos']); - } - - } - public function syncFromViewServices(string $viewName = 'public.view_services'): int { - if (! preg_match('/^[A-Za-z0-9_.]+$/', $viewName)) { + if (!preg_match('/^[A-Za-z0-9_.]+$/', $viewName)) { throw new \InvalidArgumentException('Invalid view name'); } - $connection = $this->em->getConnection(); $sql = sprintf( 'INSERT INTO site_services ( id, @@ -533,6 +201,6 @@ final class SiteServiceCrudService $viewName ); - return (int) $connection->executeStatement($sql); + return (int) $this->em->getConnection()->executeStatement($sql); } }