diff --git a/.gitignore b/.gitignore index 21c50bf..844c325 100644 --- a/.gitignore +++ b/.gitignore @@ -28,7 +28,4 @@ yarn.lock /assets/vendor/ ###< symfony/asset-mapper ### -/php: - -.cursorignore -.env \ No newline at end of file +/php: \ No newline at end of file diff --git a/config/packages/nelmio_api_doc.yaml b/config/packages/nelmio_api_doc.yaml index af132f8..26c60a8 100644 --- a/config/packages/nelmio_api_doc.yaml +++ b/config/packages/nelmio_api_doc.yaml @@ -16,11 +16,5 @@ nelmio_api_doc: '^/specialist/list$', '^/specialist/schedule$', '^/pricelist/list$', - '^/pricelist/department$', - '^/news($|/)', - '^/promo($|/)', - '^/disease($|/)', - '^/medical-center($|/)', - '^/article($|/)', - '^/site-services($|/)' - ] + '^/pricelist/department$' + ] \ No newline at end of file diff --git a/migrations/Version20260515142000.php b/migrations/Version20260515142000.php deleted file mode 100644 index e3fb5e2..0000000 --- a/migrations/Version20260515142000.php +++ /dev/null @@ -1,53 +0,0 @@ -addSql(sprintf('CREATE SEQUENCE IF NOT EXISTS %s OWNED BY %s.id', $sequence, $table)); - $this->addSql(sprintf( - 'SELECT setval(\'%s\', COALESCE((SELECT MAX(id) FROM %s), 0) + 1, false)', - $sequence, - $table, - )); - $this->addSql(sprintf( - 'ALTER TABLE %s ALTER COLUMN id SET DEFAULT nextval(\'%s\')', - $table, - $sequence, - )); - } - } - - public function down(Schema $schema): void - { - foreach (array_reverse(self::TABLES) as $table) { - $sequence = $table . '_id_seq'; - - $this->addSql(sprintf('ALTER TABLE %s ALTER COLUMN id DROP DEFAULT', $table)); - $this->addSql(sprintf('DROP SEQUENCE IF EXISTS %s', $sequence)); - } - } -} diff --git a/src/Controller/ArticleController.php b/src/Controller/ArticleController.php index 5ab7989..ef454fe 100644 --- a/src/Controller/ArticleController.php +++ b/src/Controller/ArticleController.php @@ -2,46 +2,54 @@ namespace App\Controller; -use App\Dto\Content\ContentFilterDto; use App\Entity\Article; use App\Repository\ArticleRepository; -use App\Service\Crud\CrudResponder; -use App\Service\Pagination\Paginator; -use Nelmio\ApiDocBundle\Attribute\Model; -use OpenApi\Attributes as OA; +use Doctrine\ORM\EntityManagerInterface; 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 { - private const READ_GROUPS = ['article:read']; - private const WRITE_GROUPS = ['article:write']; - public function __construct( - private readonly CrudResponder $crud, - private readonly Paginator $paginator, - ) { - } + private EntityManagerInterface $em, + private ValidatorInterface $validator, + private SerializerInterface $serializer + ) { } - #[OA\Tag(name: 'Статьи')] - #[OA\Parameter(name: 'page', in: 'query', schema: new OA\Schema(type: 'integer'))] - #[OA\Parameter(name: 'limit', in: 'query', schema: new OA\Schema(type: 'integer'))] - #[OA\Parameter(name: 'regionId', in: 'query', schema: new OA\Schema(type: 'integer'))] - #[OA\Parameter(name: 'active', in: 'query', schema: new OA\Schema(type: 'boolean'))] - #[OA\Parameter(name: 'alias', in: 'query', schema: new OA\Schema(type: 'string'))] - #[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 { - $qb = $repository->createFilteredQueryBuilder(ContentFilterDto::fromRequest($request)); + $page = max(1, (int) $request->query->get('page', 1)); + $limit = min(100, max(1, (int) $request->query->get('limit', 20))); - return $this->json($this->paginator->paginateWithLegacyMeta($qb, $request), Response::HTTP_OK, [], [ - 'groups' => self::READ_GROUPS, + $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'] ]); } @@ -52,36 +60,99 @@ final class ArticleController extends AbstractController if (!$article) { throw $this->createNotFoundException('Статья не найдена'); } - - return $this->crud->read($article, self::READ_GROUPS); + return $this->json($article, Response::HTTP_OK, [], [ + 'groups' => ['article:read'] + ]); } #[Route('/{id}', name: 'article_show', methods: ['GET'], requirements: ['id' => '\d+'])] public function show(Article $article): JsonResponse { - return $this->crud->read($article, self::READ_GROUPS); + return $this->json($article, Response::HTTP_OK, [], [ + 'groups' => ['article:read'] + ]); } #[IsGranted('ROLE_ADMIN')] - #[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: Article::class, groups: self::WRITE_GROUPS)))] #[Route('/create', name: 'article_create', methods: ['POST'])] public function create(Request $request): JsonResponse { - return $this->crud->create($request, Article::class, self::WRITE_GROUPS, self::READ_GROUPS); + 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); + } } #[IsGranted('ROLE_ADMIN')] - #[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: Article::class, groups: self::WRITE_GROUPS)))] #[Route('/{id}', name: 'article_update', methods: ['PUT'], requirements: ['id' => '\d+'])] public function update(Request $request, Article $article): JsonResponse { - return $this->crud->update($request, $article, self::WRITE_GROUPS, self::READ_GROUPS); + 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); + } } #[IsGranted('ROLE_ADMIN')] #[Route('/{id}', name: 'article_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])] public function delete(Article $article): JsonResponse { - return $this->crud->delete($article); + 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); + } } } diff --git a/src/Controller/DiseaseController.php b/src/Controller/DiseaseController.php index 34ccc34..04b4ce0 100644 --- a/src/Controller/DiseaseController.php +++ b/src/Controller/DiseaseController.php @@ -2,13 +2,8 @@ namespace App\Controller; -use App\Dto\Content\ContentFilterDto; use App\Entity\Disease; -use App\Repository\DiseaseRepository; -use App\Service\Crud\CrudResponder; -use App\Service\Pagination\Paginator; -use Nelmio\ApiDocBundle\Attribute\Model; -use OpenApi\Attributes as OA; +use App\Service\DiseaseCrudService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -19,57 +14,90 @@ 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 readonly CrudResponder $crud, - private readonly Paginator $paginator, + private DiseaseCrudService $diseaseCrud, ) { } - #[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, DiseaseRepository $repository): JsonResponse + public function list(Request $request): JsonResponse { - $qb = $repository->createFilteredQueryBuilder(ContentFilterDto::fromRequest($request)); + $page = $request->query->getInt('page', 1); + $perPage = min($request->query->getInt('perPage', 100), 500); + $regionId = $request->query->getInt('regionId', 0) ?: null; - return $this->json($this->paginator->paginate($qb, $request), Response::HTTP_OK, [], [ - 'groups' => self::READ_GROUPS, + $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'], ]); } #[Route('/{id}', name: 'disease_show', methods: ['GET'], requirements: ['id' => '\d+'])] public function show(Disease $disease): JsonResponse { - return $this->crud->read($disease, self::READ_GROUPS); + return $this->json($disease, Response::HTTP_OK, [], [ + 'groups' => ['disease:read'], + ]); } #[IsGranted('ROLE_ADMIN')] - #[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: Disease::class, groups: self::WRITE_GROUPS)))] #[Route('/create', name: 'disease_create', methods: ['POST'])] public function create(Request $request): JsonResponse { - return $this->crud->create($request, Disease::class, self::WRITE_GROUPS, self::READ_GROUPS); + $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'], + ]); } #[IsGranted('ROLE_ADMIN')] - #[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: Disease::class, groups: self::WRITE_GROUPS)))] #[Route('/{id}', name: 'disease_update', methods: ['PUT'], requirements: ['id' => '\d+'])] - public function update(Request $request, Disease $disease): JsonResponse + public function update(Disease $disease, Request $request): JsonResponse { - return $this->crud->update($request, $disease, self::WRITE_GROUPS, self::READ_GROUPS); + $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'], + ]); } #[IsGranted('ROLE_ADMIN')] #[Route('/{id}', name: 'disease_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])] public function delete(Disease $disease): JsonResponse { - return $this->crud->delete($disease); + $this->diseaseCrud->delete($disease); + + return new JsonResponse(null, Response::HTTP_NO_CONTENT); } } diff --git a/src/Controller/MedicalCenterController.php b/src/Controller/MedicalCenterController.php index 111bd43..0492347 100644 --- a/src/Controller/MedicalCenterController.php +++ b/src/Controller/MedicalCenterController.php @@ -2,13 +2,8 @@ namespace App\Controller; -use App\Dto\Content\ContentFilterDto; use App\Entity\MedicalCenter; -use App\Repository\MedicalCenterRepository; -use App\Service\Crud\CrudResponder; -use App\Service\Pagination\Paginator; -use Nelmio\ApiDocBundle\Attribute\Model; -use OpenApi\Attributes as OA; +use App\Service\MedicalCenterCrudService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -19,57 +14,72 @@ 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 readonly CrudResponder $crud, - private readonly Paginator $paginator, + private MedicalCenterCrudService $medicalCenterCrud, ) { } - #[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', description: 'Если не передан — фильтр active=true (как в старом API).', 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, MedicalCenterRepository $repository): JsonResponse + public function list(Request $request): JsonResponse { - $qb = $repository->createFilteredQueryBuilder(ContentFilterDto::fromRequest($request, true)); + $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); + } - return $this->json($this->paginator->paginate($qb, $request), Response::HTTP_OK, [], [ - 'groups' => self::READ_GROUPS, + return $this->json(['data' => $this->medicalCenterCrud->getList($regionId > 0 ? $regionId : null, $active)], Response::HTTP_OK, [], [ + 'groups' => ['medical_center:read'], ]); } #[Route('/{id}', name: 'medical_center_show', methods: ['GET'], requirements: ['id' => '\d+'])] public function show(MedicalCenter $medicalCenter): JsonResponse { - return $this->crud->read($medicalCenter, self::READ_GROUPS); + return $this->json($medicalCenter, Response::HTTP_OK, [], [ + 'groups' => ['medical_center:read'], + ]); } #[IsGranted('ROLE_ADMIN')] - #[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: MedicalCenter::class, groups: self::WRITE_GROUPS)))] #[Route('/create', name: 'medical_center_create', methods: ['POST'])] public function create(Request $request): JsonResponse { - return $this->crud->create($request, MedicalCenter::class, self::WRITE_GROUPS, self::READ_GROUPS); + $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'], + ]); } #[IsGranted('ROLE_ADMIN')] - #[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: MedicalCenter::class, groups: self::WRITE_GROUPS)))] #[Route('/{id}', name: 'medical_center_update', methods: ['PUT'], requirements: ['id' => '\d+'])] - public function update(Request $request, MedicalCenter $medicalCenter): JsonResponse + public function update(MedicalCenter $medicalCenter, Request $request): JsonResponse { - return $this->crud->update($request, $medicalCenter, self::WRITE_GROUPS, self::READ_GROUPS); + $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'], + ]); } #[IsGranted('ROLE_ADMIN')] #[Route('/{id}', name: 'medical_center_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])] public function delete(MedicalCenter $medicalCenter): JsonResponse { - return $this->crud->delete($medicalCenter); + $this->medicalCenterCrud->delete($medicalCenter); + + return new JsonResponse(null, Response::HTTP_NO_CONTENT); } } diff --git a/src/Controller/NewsController.php b/src/Controller/NewsController.php index fd9fb10..c0c8c4e 100644 --- a/src/Controller/NewsController.php +++ b/src/Controller/NewsController.php @@ -2,13 +2,8 @@ namespace App\Controller; -use App\Dto\Content\ContentFilterDto; use App\Entity\News; -use App\Repository\NewsRepository; -use App\Service\Crud\CrudResponder; -use App\Service\Pagination\Paginator; -use Nelmio\ApiDocBundle\Attribute\Model; -use OpenApi\Attributes as OA; +use App\Service\NewsCrudService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -19,57 +14,72 @@ 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 readonly CrudResponder $crud, - private readonly Paginator $paginator, + private NewsCrudService $newsCrud, ) { } - #[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', description: 'Если не передан — фильтр active=true (как в старом API).', 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, NewsRepository $repository): JsonResponse + public function list(Request $request): JsonResponse { - $qb = $repository->createFilteredQueryBuilder(ContentFilterDto::fromRequest($request, true)); + $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); + } - return $this->json($this->paginator->paginate($qb, $request), Response::HTTP_OK, [], [ - 'groups' => self::READ_GROUPS, + return $this->json(['data' => $this->newsCrud->getList($regionId > 0 ? $regionId : null, $active)], Response::HTTP_OK, [], [ + 'groups' => ['news:read'], ]); } #[Route('/{id}', name: 'news_show', methods: ['GET'], requirements: ['id' => '\d+'])] public function show(News $news): JsonResponse { - return $this->crud->read($news, self::READ_GROUPS); + return $this->json($news, Response::HTTP_OK, [], [ + 'groups' => ['news:read'], + ]); } #[IsGranted('ROLE_ADMIN')] - #[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: News::class, groups: self::WRITE_GROUPS)))] #[Route('/create', name: 'news_create', methods: ['POST'])] public function create(Request $request): JsonResponse { - return $this->crud->create($request, News::class, self::WRITE_GROUPS, self::READ_GROUPS); + $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'], + ]); } #[IsGranted('ROLE_ADMIN')] - #[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: News::class, groups: self::WRITE_GROUPS)))] #[Route('/{id}', name: 'news_update', methods: ['PUT'], requirements: ['id' => '\d+'])] - public function update(Request $request, News $news): JsonResponse + public function update(News $news, Request $request): JsonResponse { - return $this->crud->update($request, $news, self::WRITE_GROUPS, self::READ_GROUPS); + $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'], + ]); } #[IsGranted('ROLE_ADMIN')] #[Route('/{id}', name: 'news_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])] public function delete(News $news): JsonResponse { - return $this->crud->delete($news); + $this->newsCrud->delete($news); + + return new JsonResponse(null, Response::HTTP_NO_CONTENT); } } diff --git a/src/Controller/PromoController.php b/src/Controller/PromoController.php index 48aafdd..dee5970 100644 --- a/src/Controller/PromoController.php +++ b/src/Controller/PromoController.php @@ -2,13 +2,8 @@ namespace App\Controller; -use App\Dto\Content\ContentFilterDto; use App\Entity\Promo; -use App\Repository\PromoRepository; -use App\Service\Crud\CrudResponder; -use App\Service\Pagination\Paginator; -use Nelmio\ApiDocBundle\Attribute\Model; -use OpenApi\Attributes as OA; +use App\Service\PromoCrudService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -19,57 +14,72 @@ 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 readonly CrudResponder $crud, - private readonly Paginator $paginator, + private PromoCrudService $promoCrud, ) { } - #[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', description: 'Если не передан — фильтр active=true (как в старом API).', 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, PromoRepository $repository): JsonResponse + public function list(Request $request): JsonResponse { - $qb = $repository->createFilteredQueryBuilder(ContentFilterDto::fromRequest($request, true)); + $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); + } - return $this->json($this->paginator->paginate($qb, $request), Response::HTTP_OK, [], [ - 'groups' => self::READ_GROUPS, + return $this->json(['data' => $this->promoCrud->getList($regionId > 0 ? $regionId : null, $active)], Response::HTTP_OK, [], [ + 'groups' => ['promo:read'], ]); } #[Route('/{id}', name: 'promo_show', methods: ['GET'], requirements: ['id' => '\d+'])] public function show(Promo $promo): JsonResponse { - return $this->crud->read($promo, self::READ_GROUPS); + return $this->json($promo, Response::HTTP_OK, [], [ + 'groups' => ['promo:read'], + ]); } #[IsGranted('ROLE_ADMIN')] - #[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: Promo::class, groups: self::WRITE_GROUPS)))] #[Route('/create', name: 'promo_create', methods: ['POST'])] public function create(Request $request): JsonResponse { - return $this->crud->create($request, Promo::class, self::WRITE_GROUPS, self::READ_GROUPS); + $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'], + ]); } #[IsGranted('ROLE_ADMIN')] - #[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: Promo::class, groups: self::WRITE_GROUPS)))] #[Route('/{id}', name: 'promo_update', methods: ['PUT'], requirements: ['id' => '\d+'])] - public function update(Request $request, Promo $promo): JsonResponse + public function update(Promo $promo, Request $request): JsonResponse { - return $this->crud->update($request, $promo, self::WRITE_GROUPS, self::READ_GROUPS); + $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'], + ]); } #[IsGranted('ROLE_ADMIN')] #[Route('/{id}', name: 'promo_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])] public function delete(Promo $promo): JsonResponse { - return $this->crud->delete($promo); + $this->promoCrud->delete($promo); + + return new JsonResponse(null, Response::HTTP_NO_CONTENT); } } diff --git a/src/Controller/SiteServiceController.php b/src/Controller/SiteServiceController.php index 5443153..f078bad 100644 --- a/src/Controller/SiteServiceController.php +++ b/src/Controller/SiteServiceController.php @@ -2,13 +2,8 @@ namespace App\Controller; -use App\Dto\Content\ContentFilterDto; use App\Entity\SiteService; -use App\Repository\SiteServiceRepository; -use App\Service\Crud\CrudResponder; -use App\Service\Pagination\Paginator; -use Nelmio\ApiDocBundle\Attribute\Model; -use OpenApi\Attributes as OA; +use App\Service\SiteServiceCrudService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -19,57 +14,91 @@ 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 readonly CrudResponder $crud, - private readonly Paginator $paginator, + private SiteServiceCrudService $siteServiceCrud, ) { } - #[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', description: 'Если не передан — фильтр active=true (как в старом API).', 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, SiteServiceRepository $repository): JsonResponse + public function list(Request $request): JsonResponse { - $qb = $repository->createFilteredQueryBuilder(ContentFilterDto::fromRequest($request, true)); + $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); + } - return $this->json($this->paginator->paginate($qb, $request), Response::HTTP_OK, [], [ - 'groups' => self::READ_GROUPS, + $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'], ]); } #[Route('/{id}', name: 'site_service_show', methods: ['GET'], requirements: ['id' => '\d+'])] public function show(SiteService $siteService): JsonResponse { - return $this->crud->read($siteService, self::READ_GROUPS); + return $this->json($siteService, Response::HTTP_OK, [], [ + 'groups' => ['site_service:read'], + ]); } #[IsGranted('ROLE_ADMIN')] - #[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: SiteService::class, groups: self::WRITE_GROUPS)))] #[Route('/create', name: 'site_service_create', methods: ['POST'])] public function create(Request $request): JsonResponse { - return $this->crud->create($request, SiteService::class, self::WRITE_GROUPS, self::READ_GROUPS); + $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'], + ]); } #[IsGranted('ROLE_ADMIN')] - #[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: SiteService::class, groups: self::WRITE_GROUPS)))] #[Route('/{id}', name: 'site_service_update', methods: ['PUT'], requirements: ['id' => '\d+'])] - public function update(Request $request, SiteService $siteService): JsonResponse + public function update(SiteService $siteService, Request $request): JsonResponse { - return $this->crud->update($request, $siteService, self::WRITE_GROUPS, self::READ_GROUPS); + $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'], + ]); } #[IsGranted('ROLE_ADMIN')] #[Route('/{id}', name: 'site_service_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])] public function delete(SiteService $siteService): JsonResponse { - return $this->crud->delete($siteService); + $this->siteServiceCrud->delete($siteService); + + return new JsonResponse(null, Response::HTTP_NO_CONTENT); } } diff --git a/src/Dto/Content/ContentFilterDto.php b/src/Dto/Content/ContentFilterDto.php deleted file mode 100644 index 1902b5f..0000000 --- a/src/Dto/Content/ContentFilterDto.php +++ /dev/null @@ -1,81 +0,0 @@ -query->get('active')); - - return new self( - regionId: self::positiveInt($request->query->get('regionId', $request->query->get('region_id'))), - active: $active ?? $defaultActive, - alias: self::nonEmptyString($request->query->get('alias')), - search: self::nonEmptyString($request->query->get('search', $request->query->get('q'))), - ); - } - - /** - * Symfony QueryBag может отдать массив при ?regionId[]=… — не передаём его в is_numeric (TypeError в PHP 8). - */ - private static function positiveInt(mixed $value): ?int - { - if ($value === null || $value === '' || !is_scalar($value) || !is_numeric($value)) { - return null; - } - - $value = (int) $value; - - return $value > 0 ? $value : null; - } - - /** - * При ?active[]=… query->get вернёт массив — отбрасываем без вызова filter_var по нему. - */ - private static function nullableBool(mixed $value): ?bool - { - if ($value === null || $value === '') { - return null; - } - - if (!is_scalar($value)) { - return null; - } - - if (is_bool($value)) { - return $value; - } - - return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); - } - - private static function nonEmptyString(mixed $value): ?string - { - if (!is_string($value)) { - return null; - } - - $value = trim($value); - - return $value !== '' ? $value : null; - } -} diff --git a/src/Entity/Article.php b/src/Entity/Article.php index 3f37913..dffa469 100644 --- a/src/Entity/Article.php +++ b/src/Entity/Article.php @@ -2,7 +2,6 @@ namespace App\Entity; -use App\Entity\Behavior\UpdateTimestampTrait; use App\Repository\ArticleRepository; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; @@ -13,11 +12,8 @@ use Symfony\Component\Validator\Constraints as Assert; #[ORM\Table(name: 'article')] #[ORM\Index(name: 'idx_article_region_id', columns: ['region_id'])] #[ORM\Index(name: 'idx_article_active', columns: ['active'])] -#[ORM\HasLifecycleCallbacks] class Article { - use UpdateTimestampTrait; - #[Groups(['article:read'])] #[ORM\Id] #[ORM\GeneratedValue(strategy: "IDENTITY")] @@ -60,8 +56,8 @@ class Article #[ORM\Column(type: Types::TEXT, nullable: true)] private ?string $content = null; - #[Groups(['article:read'])] - #[ORM\Column(name: 'update_at', type: Types::DATETIME_IMMUTABLE, nullable: true)] + #[Groups(['article:read', 'article:write'])] + #[ORM\Column(name: 'update_at', type: Types::DATETIME_MUTABLE, nullable: true)] private ?\DateTimeInterface $updateAt = null; public function getId(): ?int diff --git a/src/Entity/Behavior/UpdateTimestampTrait.php b/src/Entity/Behavior/UpdateTimestampTrait.php deleted file mode 100644 index f68363f..0000000 --- a/src/Entity/Behavior/UpdateTimestampTrait.php +++ /dev/null @@ -1,29 +0,0 @@ -updateAt === null) { - $this->updateAt = new \DateTimeImmutable(); - } - } - - #[ORM\PreUpdate] - public function refreshUpdateAt(): void - { - $this->updateAt = new \DateTimeImmutable(); - } -} diff --git a/src/Entity/Disease.php b/src/Entity/Disease.php index 3a33ac3..26fc0bd 100644 --- a/src/Entity/Disease.php +++ b/src/Entity/Disease.php @@ -2,7 +2,6 @@ namespace App\Entity; -use App\Entity\Behavior\UpdateTimestampTrait; use App\Repository\DiseaseRepository; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; @@ -12,14 +11,10 @@ use Symfony\Component\Serializer\Annotation\Groups; #[ORM\Table(name: 'disease')] #[ORM\Index(name: 'idx_disease_region_id', columns: ['region_id'])] #[ORM\Index(name: 'idx_disease_active', columns: ['active'])] -#[ORM\HasLifecycleCallbacks] class Disease { - use UpdateTimestampTrait; - #[Groups(['disease:read'])] #[ORM\Id] - #[ORM\GeneratedValue(strategy: "IDENTITY")] #[ORM\Column(type: Types::INTEGER)] private ?int $id = null; @@ -47,8 +42,8 @@ class Disease #[ORM\Column(type: Types::TEXT, nullable: true)] private ?string $anons = null; - #[Groups(['disease:read'])] - #[ORM\Column(name: 'update_at', type: Types::DATETIME_IMMUTABLE, nullable: true)] + #[Groups(['disease:read', 'disease:write'])] + #[ORM\Column(name: 'update_at', type: Types::DATETIME_MUTABLE, nullable: true)] private ?\DateTimeInterface $updateAt = null; #[Groups(['disease:read', 'disease:write'])] diff --git a/src/Entity/MedicalCenter.php b/src/Entity/MedicalCenter.php index c3411cf..b116c78 100644 --- a/src/Entity/MedicalCenter.php +++ b/src/Entity/MedicalCenter.php @@ -2,7 +2,6 @@ namespace App\Entity; -use App\Entity\Behavior\UpdateTimestampTrait; use App\Repository\MedicalCenterRepository; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; @@ -10,13 +9,9 @@ use Symfony\Component\Serializer\Annotation\Groups; #[ORM\Entity(repositoryClass: MedicalCenterRepository::class)] #[ORM\Table(name: 'medical_center')] -#[ORM\HasLifecycleCallbacks] class MedicalCenter { - use UpdateTimestampTrait; - #[ORM\Id] - #[ORM\GeneratedValue(strategy: "IDENTITY")] #[ORM\Column(type: Types::INTEGER)] #[Groups(['medical_center:read'])] private ?int $id = null; @@ -45,8 +40,8 @@ class MedicalCenter #[Groups(['medical_center:read', 'medical_center:write'])] private ?string $content = null; - #[ORM\Column(name: 'update_at', type: Types::DATETIME_IMMUTABLE, nullable: true)] - #[Groups(['medical_center:read'])] + #[ORM\Column(name: 'update_at', type: Types::DATETIME_MUTABLE, nullable: true)] + #[Groups(['medical_center:read', 'medical_center:write'])] private ?\DateTimeInterface $updateAt = null; #[ORM\Column(name: 'kod_uslug', type: 'jsonb', nullable: true)] diff --git a/src/Entity/News.php b/src/Entity/News.php index afd8194..94dd5e9 100644 --- a/src/Entity/News.php +++ b/src/Entity/News.php @@ -2,7 +2,6 @@ namespace App\Entity; -use App\Entity\Behavior\UpdateTimestampTrait; use App\Repository\NewsRepository; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; @@ -12,13 +11,9 @@ use Symfony\Component\Serializer\Annotation\Groups; #[ORM\Table(name: 'news')] #[ORM\Index(name: 'idx_news_region_id', columns: ['region_id'])] #[ORM\Index(name: 'idx_news_active', columns: ['active'])] -#[ORM\HasLifecycleCallbacks] class News { - use UpdateTimestampTrait; - #[ORM\Id] - #[ORM\GeneratedValue(strategy: "IDENTITY")] #[ORM\Column(type: Types::INTEGER)] #[Groups(['news:read'])] private ?int $id = null; @@ -47,8 +42,8 @@ class News #[Groups(['news:read', 'news:write'])] private ?string $content = null; - #[ORM\Column(name: 'update_at', type: Types::DATETIME_IMMUTABLE, nullable: true)] - #[Groups(['news:read'])] + #[ORM\Column(name: 'update_at', type: Types::DATETIME_MUTABLE, nullable: true)] + #[Groups(['news:read', 'news:write'])] private ?\DateTimeInterface $updateAt = null; #[ORM\Column(name: 'link_el_price', type: Types::TEXT, nullable: true)] diff --git a/src/Entity/Promo.php b/src/Entity/Promo.php index e612da9..94bb004 100644 --- a/src/Entity/Promo.php +++ b/src/Entity/Promo.php @@ -2,7 +2,6 @@ namespace App\Entity; -use App\Entity\Behavior\UpdateTimestampTrait; use App\Repository\PromoRepository; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; @@ -12,13 +11,9 @@ use Symfony\Component\Serializer\Annotation\Groups; #[ORM\Table(name: 'promo')] #[ORM\Index(name: 'idx_promo_region_id', columns: ['region_id'])] #[ORM\Index(name: 'idx_promo_active', columns: ['active'])] -#[ORM\HasLifecycleCallbacks] class Promo { - use UpdateTimestampTrait; - #[ORM\Id] - #[ORM\GeneratedValue(strategy: "IDENTITY")] #[ORM\Column(type: Types::INTEGER)] #[Groups(['promo:read'])] private ?int $id = null; @@ -47,8 +42,8 @@ class Promo #[Groups(['promo:read', 'promo:write'])] private ?string $content = null; - #[ORM\Column(name: 'update_at', type: Types::DATETIME_IMMUTABLE, nullable: true)] - #[Groups(['promo:read'])] + #[ORM\Column(name: 'update_at', type: Types::DATETIME_MUTABLE, nullable: true)] + #[Groups(['promo:read', 'promo:write'])] private ?\DateTimeInterface $updateAt = null; #[ORM\Column(type: 'jsonb', nullable: true)] diff --git a/src/Entity/SiteService.php b/src/Entity/SiteService.php index b67737f..cf48e0f 100644 --- a/src/Entity/SiteService.php +++ b/src/Entity/SiteService.php @@ -2,7 +2,6 @@ namespace App\Entity; -use App\Entity\Behavior\UpdateTimestampTrait; use App\Repository\SiteServiceRepository; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; @@ -12,13 +11,9 @@ use Symfony\Component\Serializer\Annotation\Groups; #[ORM\Table(name: 'site_services')] #[ORM\Index(name: 'idx_site_services_region_id', columns: ['region_id'])] #[ORM\Index(name: 'idx_site_services_active', columns: ['active'])] -#[ORM\HasLifecycleCallbacks] class SiteService { - use UpdateTimestampTrait; - #[ORM\Id] - #[ORM\GeneratedValue(strategy: "IDENTITY")] #[ORM\Column(type: Types::INTEGER)] #[Groups(['site_service:read'])] private ?int $id = null; @@ -47,8 +42,8 @@ class SiteService #[Groups(['site_service:read', 'site_service:write'])] private ?string $content = null; - #[ORM\Column(name: 'update_at', type: Types::DATETIME_IMMUTABLE, nullable: true)] - #[Groups(['site_service:read'])] + #[ORM\Column(name: 'update_at', type: Types::DATETIME_MUTABLE, nullable: true)] + #[Groups(['site_service:read', 'site_service:write'])] private ?\DateTimeInterface $updateAt = null; #[ORM\Column(name: 'link_videoreviews', type: 'jsonb', nullable: true)] diff --git a/src/Repository/ArticleRepository.php b/src/Repository/ArticleRepository.php index e194953..697d852 100644 --- a/src/Repository/ArticleRepository.php +++ b/src/Repository/ArticleRepository.php @@ -2,10 +2,8 @@ namespace App\Repository; -use App\Dto\Content\ContentFilterDto; use App\Entity\Article; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; -use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; /** @@ -13,34 +11,63 @@ use Doctrine\Persistence\ManagerRegistry; */ class ArticleRepository extends ServiceEntityRepository { - use ContentFilterTrait; - public function __construct(ManagerRegistry $registry) { parent::__construct($registry, Article::class); } - /** - */ - public function createFilteredQueryBuilder(ContentFilterDto $filters): QueryBuilder + public function findByFilters(array $filters, int $page = 1, int $limit = 20): array { - $qb = $this->createQueryBuilder('a')->orderBy('a.id', 'DESC'); + $qb = $this->createQueryBuilder('a'); - $this->applyCommonFilters($qb, 'a', $filters); + 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 $qb; + $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(); } - /** - * Поиск статьи по alias с учётом возможных вариантов написания (исторический функционал). - */ public function findOneByAlias(string $alias): ?Article { $alias = trim($alias); if ($alias === '') { return null; } - $variants = [ $alias, $alias . '-', @@ -52,18 +79,16 @@ class ArticleRepository extends ServiceEntityRepository return $article; } } - - // Фолбэк по TRIM(alias) в БД для совместимости со старыми данными. + // Поиск по TRIM(alias) в БД (нативный SQL для совместимости с PostgreSQL) $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/ContentFilterTrait.php b/src/Repository/ContentFilterTrait.php deleted file mode 100644 index 87366e9..0000000 --- a/src/Repository/ContentFilterTrait.php +++ /dev/null @@ -1,58 +0,0 @@ - 0; - * - active: bool; - * - alias: точное совпадение; - * - search / q: LIKE по lower-case значению заданного поля (по умолчанию `name`). - * - * Поле поиска параметризовано через $searchField на случай сущностей, - * где основное текстовое поле называется иначе (например, `title`). - * Если у сущности нет такого свойства, Doctrine упадёт с QueryException — это - * лучше ловится тестами на этапе разработки, чем 500 в проде. - * - * Важно: LOWER($alias.$searchField) при больших таблицах требует функционального - * индекса в PostgreSQL, например CREATE INDEX ... ON table (LOWER(name)). - */ -trait ContentFilterTrait -{ - private function applyCommonFilters( - QueryBuilder $qb, - string $alias, - ContentFilterDto $filters, - string $searchField = 'name', - ): void { - if ($filters->regionId !== null) { - $qb->andWhere("$alias.regionId = :regionId") - ->setParameter('regionId', $filters->regionId); - } - - if ($filters->active !== null) { - $qb->andWhere("$alias.active = :active") - ->setParameter('active', $filters->active); - } - - if ($filters->alias !== null) { - $qb->andWhere("$alias.alias = :aliasValue") - ->setParameter('aliasValue', $filters->alias); - } - - if ($filters->search !== null) { - $qb->andWhere("LOWER($alias.$searchField) LIKE :search") - ->setParameter('search', '%' . mb_strtolower($filters->search) . '%'); - } - } -} diff --git a/src/Repository/DiseaseRepository.php b/src/Repository/DiseaseRepository.php index 33dd6b1..24f4994 100644 --- a/src/Repository/DiseaseRepository.php +++ b/src/Repository/DiseaseRepository.php @@ -2,10 +2,8 @@ namespace App\Repository; -use App\Dto\Content\ContentFilterDto; use App\Entity\Disease; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; -use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; /** @@ -16,21 +14,8 @@ use Doctrine\Persistence\ManagerRegistry; */ class DiseaseRepository extends ServiceEntityRepository { - use ContentFilterTrait; - public function __construct(ManagerRegistry $registry) { parent::__construct($registry, Disease::class); } - - /** - */ - public function createFilteredQueryBuilder(ContentFilterDto $filters): QueryBuilder - { - $qb = $this->createQueryBuilder('d')->orderBy('d.id', 'ASC'); - - $this->applyCommonFilters($qb, 'd', $filters); - - return $qb; - } } diff --git a/src/Repository/MedicalCenterRepository.php b/src/Repository/MedicalCenterRepository.php index 021af74..7088a7c 100644 --- a/src/Repository/MedicalCenterRepository.php +++ b/src/Repository/MedicalCenterRepository.php @@ -2,10 +2,8 @@ namespace App\Repository; -use App\Dto\Content\ContentFilterDto; use App\Entity\MedicalCenter; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; -use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; /** @@ -16,21 +14,8 @@ use Doctrine\Persistence\ManagerRegistry; */ class MedicalCenterRepository extends ServiceEntityRepository { - use ContentFilterTrait; - public function __construct(ManagerRegistry $registry) { parent::__construct($registry, MedicalCenter::class); } - - /** - */ - public function createFilteredQueryBuilder(ContentFilterDto $filters): QueryBuilder - { - $qb = $this->createQueryBuilder('m')->orderBy('m.id', 'DESC'); - - $this->applyCommonFilters($qb, 'm', $filters); - - return $qb; - } } diff --git a/src/Repository/NewsRepository.php b/src/Repository/NewsRepository.php index 4520283..9607b31 100644 --- a/src/Repository/NewsRepository.php +++ b/src/Repository/NewsRepository.php @@ -2,10 +2,8 @@ namespace App\Repository; -use App\Dto\Content\ContentFilterDto; use App\Entity\News; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; -use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; /** @@ -16,25 +14,8 @@ use Doctrine\Persistence\ManagerRegistry; */ class NewsRepository extends ServiceEntityRepository { - use ContentFilterTrait; - public function __construct(ManagerRegistry $registry) { parent::__construct($registry, News::class); } - - /** - * Готовит QueryBuilder под пагинацию (Pagerfanta\QueryAdapter). - * - * Поддерживаемые фильтры: regionId, active (по умолчанию true), alias, search. - * - */ - public function createFilteredQueryBuilder(ContentFilterDto $filters): QueryBuilder - { - $qb = $this->createQueryBuilder('n')->orderBy('n.id', 'DESC'); - - $this->applyCommonFilters($qb, 'n', $filters); - - return $qb; - } } diff --git a/src/Repository/PromoRepository.php b/src/Repository/PromoRepository.php index 3d73d2b..5a5c4c6 100644 --- a/src/Repository/PromoRepository.php +++ b/src/Repository/PromoRepository.php @@ -2,10 +2,8 @@ namespace App\Repository; -use App\Dto\Content\ContentFilterDto; use App\Entity\Promo; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; -use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; /** @@ -16,21 +14,8 @@ use Doctrine\Persistence\ManagerRegistry; */ class PromoRepository extends ServiceEntityRepository { - use ContentFilterTrait; - public function __construct(ManagerRegistry $registry) { parent::__construct($registry, Promo::class); } - - /** - */ - public function createFilteredQueryBuilder(ContentFilterDto $filters): QueryBuilder - { - $qb = $this->createQueryBuilder('p')->orderBy('p.id', 'DESC'); - - $this->applyCommonFilters($qb, 'p', $filters); - - return $qb; - } } diff --git a/src/Repository/SiteServiceRepository.php b/src/Repository/SiteServiceRepository.php index 73d834a..1a07399 100644 --- a/src/Repository/SiteServiceRepository.php +++ b/src/Repository/SiteServiceRepository.php @@ -2,10 +2,8 @@ namespace App\Repository; -use App\Dto\Content\ContentFilterDto; use App\Entity\SiteService; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; -use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; /** @@ -16,21 +14,8 @@ use Doctrine\Persistence\ManagerRegistry; */ class SiteServiceRepository extends ServiceEntityRepository { - use ContentFilterTrait; - public function __construct(ManagerRegistry $registry) { parent::__construct($registry, SiteService::class); } - - /** - */ - public function createFilteredQueryBuilder(ContentFilterDto $filters): QueryBuilder - { - $qb = $this->createQueryBuilder('s')->orderBy('s.id', 'ASC'); - - $this->applyCommonFilters($qb, 's', $filters); - - return $qb; - } } diff --git a/src/Service/Crud/CrudResponder.php b/src/Service/Crud/CrudResponder.php deleted file mode 100644 index a775767..0000000 --- a/src/Service/Crud/CrudResponder.php +++ /dev/null @@ -1,195 +0,0 @@ - $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, - ): JsonResponse { - $payload = $this->decodePayload($request); - if ($payload === null) { - return $this->jsonError('Ожидается JSON-объект в теле запроса', Response::HTTP_BAD_REQUEST); - } - unset($payload['id']); - - try { - /** @var T $entity */ - $entity = $this->denormalizer->denormalize( - $payload, - $entityClass, - null, - [ - AbstractNormalizer::GROUPS => $writeGroups, - ], - ); - } catch (SerializerExceptionInterface $e) { - return $this->jsonError('Ошибка десериализации: ' . $e->getMessage(), Response::HTTP_BAD_REQUEST); - } - - 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 { - $payload = $this->decodePayload($request); - if ($payload === null) { - return $this->jsonError('Ожидается JSON-объект в теле запроса', Response::HTTP_BAD_REQUEST); - } - unset($payload['id']); - - try { - $this->denormalizer->denormalize( - $payload, - $entity::class, - null, - [ - 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 - { - try { - $this->em->remove($entity); - $this->em->flush(); - } catch (DbalException $e) { - // Сохраняем легаси-контракт: при FK / NOT NULL / unique ошибках БД - // отдаём 500 + {error, message}. См. старый ArticleController::delete. - return new JsonResponse( - ['error' => 'Ошибка при удалении записи', 'message' => $e->getMessage()], - Response::HTTP_INTERNAL_SERVER_ERROR, - ); - } - - return new JsonResponse(null, Response::HTTP_NO_CONTENT); - } - - /** - * @return array|null null если тело не является JSON-объектом - * - * Ловим как нативный \JsonException, так и Symfony\...\HttpFoundation\Exception\JsonException - * (последний наследует UnexpectedValueException, а не \JsonException, и без - * широкого перехвата Symfony ErrorListener перехватит ошибку до нашего try/catch). - */ - private function decodePayload(Request $request): ?array - { - try { - return $request->toArray(); - } catch (JsonException|\UnexpectedValueException) { - return null; - } - } - - private function validate(object $entity): ?JsonResponse - { - $errors = $this->validator->validate($entity); - if (count($errors) === 0) { - return null; - } - - // BC: легаси-контроллеры возвращали именно сериализованный ConstraintViolationList - // с кодом 400. Этот же формат продолжаем отдавать здесь, чтобы фронтенду - // не пришлось переписывать парсинг ошибок. - $json = $this->serializer->serialize($errors, 'json'); - - return new JsonResponse($json, Response::HTTP_BAD_REQUEST, [], true); - } - - /** - * @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 ff42797..01a0de1 100644 --- a/src/Service/DiseaseCrudService.php +++ b/src/Service/DiseaseCrudService.php @@ -2,26 +2,206 @@ 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, @@ -102,6 +282,6 @@ final class DiseaseCrudService $viewName ); - return (int) $this->em->getConnection()->executeStatement($sql); + return (int) $connection->executeStatement($sql); } } diff --git a/src/Service/MedicalCenterCrudService.php b/src/Service/MedicalCenterCrudService.php index 1dc1be7..f5d6ed9 100644 --- a/src/Service/MedicalCenterCrudService.php +++ b/src/Service/MedicalCenterCrudService.php @@ -2,127 +2,312 @@ 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, - ) { - } + public function __construct( + private EntityManagerInterface $em, + private MedicalCenterRepository $medicalCenterRepository + ) { + } - public function syncFromViewCenters(string $viewName = 'public.view_centers'): int - { - if (!preg_match('/^[A-Za-z0-9_\.]+$/', $viewName)) { - throw new \InvalidArgumentException('Invalid view name'); - } + /** + * @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; + } - $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 $this->medicalCenterRepository->findBy($criteria, ['id' => 'ASC']); + } - return (int) $this->em->getConnection()->executeStatement($sql); - } + 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); + } } + diff --git a/src/Service/NewsCrudService.php b/src/Service/NewsCrudService.php index c5a7fd0..988f1ed 100644 --- a/src/Service/NewsCrudService.php +++ b/src/Service/NewsCrudService.php @@ -2,28 +2,148 @@ 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, @@ -80,6 +200,6 @@ final class NewsCrudService $viewName ); - return (int) $this->em->getConnection()->executeStatement($sql); + return (int) $connection->executeStatement($sql); } } diff --git a/src/Service/Pagination/Paginator.php b/src/Service/Pagination/Paginator.php deleted file mode 100644 index bb53ee6..0000000 --- a/src/Service/Pagination/Paginator.php +++ /dev/null @@ -1,107 +0,0 @@ - [...], 'pagination' => [...]] в едином формате для новых list-контрактов. - */ -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(), - ], - ]; - } - - /** - * Legacy-формат для ArticleController. - * - * Старый контракт /article/list уже использовался клиентами: - * - размер страницы приходит в query-параметре limit; - * - метаданные лежат в ключе meta; - * - поля называются total/page/limit/totalPages. - * - * @return array{data: list, meta: array{total: int, page: int, limit: int, totalPages: int}} - */ - public function paginateWithLegacyMeta( - QueryBuilder $qb, - Request $request, - int $defaultLimit = 20, - int $maxLimit = 100, - ): array { - $page = max(1, $request->query->getInt('page', 1)); - $limit = min( - max(1, $request->query->getInt('limit', $defaultLimit)), - $maxLimit, - ); - - $pagerfanta = (new Pagerfanta(new QueryAdapter($qb))) - ->setMaxPerPage($limit); - - try { - $pagerfanta->setCurrentPage($page); - } catch (NotValidCurrentPageException) { - $pagerfanta->setCurrentPage(max(1, $pagerfanta->getNbPages())); - } - - return [ - 'data' => iterator_to_array($pagerfanta->getCurrentPageResults(), false), - 'meta' => [ - 'total' => $pagerfanta->getNbResults(), - 'page' => $pagerfanta->getCurrentPage(), - 'limit' => $pagerfanta->getMaxPerPage(), - 'totalPages' => $pagerfanta->getNbPages(), - ], - ]; - } -} diff --git a/src/Service/PromoCrudService.php b/src/Service/PromoCrudService.php index c6b21b6..e0f2fab 100644 --- a/src/Service/PromoCrudService.php +++ b/src/Service/PromoCrudService.php @@ -2,26 +2,148 @@ 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, @@ -78,6 +200,6 @@ final class PromoCrudService $viewName ); - return (int) $this->em->getConnection()->executeStatement($sql); + return (int) $connection->executeStatement($sql); } } diff --git a/src/Service/SiteServiceCrudService.php b/src/Service/SiteServiceCrudService.php index befae42..fc7b607 100644 --- a/src/Service/SiteServiceCrudService.php +++ b/src/Service/SiteServiceCrudService.php @@ -2,26 +2,358 @@ 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, @@ -201,6 +533,6 @@ final class SiteServiceCrudService $viewName ); - return (int) $this->em->getConnection()->executeStatement($sql); + return (int) $connection->executeStatement($sql); } }