issues/27: update crud from admin api

This commit is contained in:
Valery Petrov
2026-05-14 16:16:07 +03:00
committed by Valeriy Petrov
parent 839ccdffb5
commit bc5468e5a0
21 changed files with 755 additions and 1456 deletions
+8 -2
View File
@@ -16,5 +16,11 @@ nelmio_api_doc:
'^/specialist/list$',
'^/specialist/schedule$',
'^/pricelist/list$',
'^/pricelist/department$'
]
'^/pricelist/department$',
'^/news($|/)',
'^/promo($|/)',
'^/disease($|/)',
'^/medical-center($|/)',
'^/article($|/)',
'^/site-services($|/)'
]
+27 -102
View File
@@ -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);
}
}
+24 -56
View File
@@ -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);
}
}
+24 -38
View File
@@ -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);
}
}
+24 -38
View File
@@ -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);
}
}
+24 -38
View File
@@ -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);
}
}
+24 -57
View File
@@ -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);
}
}
+16 -43
View File
@@ -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<string, mixed> $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;
}
}
+123
View File
@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use Doctrine\ORM\QueryBuilder;
/**
* Общие фильтры для контентных репозиториев (News/Promo/Disease/MedicalCenter/Article/SiteService).
*
* Назначение — не плодить одинаковые if-блоки в каждом репозитории и сохранить
* единый контракт query-параметров для list-эндпоинтов.
*
* Поддерживается:
* - regionId целое > 0
* - active bool (по умолчанию true, если не передан)
* - alias строка (строгое равенство)
* - search LIKE по name (case-insensitive)
*
* Параметры пагинации (page/perPage) обрабатываются Paginator'ом и здесь игнорируются.
*/
final class ContentRepositoryFilter
{
/**
* @param array<string, mixed> $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<string, mixed> $filters
* @param list<string> $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<string, mixed> $filters
* @param list<string> $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<string, mixed> $filters
* @param list<string> $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;
}
}
+13
View File
@@ -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<string, mixed> $filters
*/
public function createFilteredQueryBuilder(array $filters): QueryBuilder
{
$qb = $this->createQueryBuilder('d')->orderBy('d.id', 'ASC');
ContentRepositoryFilter::applyCommon($qb, 'd', $filters);
return $qb;
}
}
@@ -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<string, mixed> $filters
*/
public function createFilteredQueryBuilder(array $filters): QueryBuilder
{
$qb = $this->createQueryBuilder('m')->orderBy('m.id', 'DESC');
ContentRepositoryFilter::applyCommon($qb, 'm', $filters);
return $qb;
}
}
+17
View File
@@ -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<string, mixed> $filters
*/
public function createFilteredQueryBuilder(array $filters): QueryBuilder
{
$qb = $this->createQueryBuilder('n')->orderBy('n.id', 'DESC');
ContentRepositoryFilter::applyCommon($qb, 'n', $filters);
return $qb;
}
}
+13
View File
@@ -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<string, mixed> $filters
*/
public function createFilteredQueryBuilder(array $filters): QueryBuilder
{
$qb = $this->createQueryBuilder('p')->orderBy('p.id', 'DESC');
ContentRepositoryFilter::applyCommon($qb, 'p', $filters);
return $qb;
}
}
+13
View File
@@ -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<string, mixed> $filters
*/
public function createFilteredQueryBuilder(array $filters): QueryBuilder
{
$qb = $this->createQueryBuilder('s')->orderBy('s.id', 'ASC');
ContentRepositoryFilter::applyCommon($qb, 's', $filters);
return $qb;
}
}
+184
View File
@@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
namespace App\Service\Crud;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\Exception\ExceptionInterface as SerializerExceptionInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* Универсальный CRUD-ответчик для тонких контент-контроллеров.
*
* Инкапсулирует общую логику десериализации тела запроса по группе :write,
* валидации сущности, persist/flush и сериализации ответа по группе :read.
*
* Контроллер становится декларативным:
* return $this->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<string> $readGroups
*/
public function read(object $entity, array $readGroups): JsonResponse
{
return $this->json($entity, Response::HTTP_OK, $readGroups);
}
/**
* @template T of object
*
* @param class-string<T> $entityClass
* @param list<string> $writeGroups
* @param list<string> $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<string> $writeGroups
* @param list<string> $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<string, mixed>|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<string> $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);
}
}
+6 -186
View File
@@ -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);
}
}
+116 -301
View File
@@ -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);
}
}
+8 -128
View File
@@ -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);
}
}
+65
View File
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace App\Service\Pagination;
use Doctrine\ORM\QueryBuilder;
use Pagerfanta\Doctrine\ORM\QueryAdapter;
use Pagerfanta\Exception\NotValidCurrentPageException;
use Pagerfanta\Pagerfanta;
use Symfony\Component\HttpFoundation\Request;
/**
* Унифицированная обёртка над Pagerfanta + QueryAdapter.
*
* Соответствует существующему стилю проекта (см. PriceListController/SpecialistController):
* читает page/perPage из Request, ограничивает perPage и возвращает массив
* ['data' => [...], 'pagination' => [...]] в едином формате для всех контроллеров.
*/
final class Paginator
{
public const DEFAULT_PER_PAGE = 50;
public const MAX_PER_PAGE = 500;
/**
* @return array{data: list<mixed>, pagination: array<string, int|bool>}
*/
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(),
],
];
}
}
+6 -128
View File
@@ -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);
}
}
+7 -339
View File
@@ -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);
}
}