|
@@ -16,5 +16,11 @@ nelmio_api_doc:
|
|
| 16 |
'^/specialist/list$',
|
| 17 |
'^/specialist/schedule$',
|
| 18 |
'^/pricelist/list$',
|
| 19 |
-
'^/pricelist/department$'
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
'^/specialist/list$',
|
| 17 |
'^/specialist/schedule$',
|
| 18 |
'^/pricelist/list$',
|
| 19 |
+
'^/pricelist/department$',
|
| 20 |
+
'^/news($|/)',
|
| 21 |
+
'^/promo($|/)',
|
| 22 |
+
'^/disease($|/)',
|
| 23 |
+
'^/medical-center($|/)',
|
| 24 |
+
'^/article($|/)',
|
| 25 |
+
'^/site-services($|/)'
|
| 26 |
+
]
|
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
declare(strict_types=1);
|
| 4 |
+
|
| 5 |
+
namespace DoctrineMigrations;
|
| 6 |
+
|
| 7 |
+
use Doctrine\DBAL\Schema\Schema;
|
| 8 |
+
use Doctrine\Migrations\AbstractMigration;
|
| 9 |
+
|
| 10 |
+
final class Version20260515142000 extends AbstractMigration
|
| 11 |
+
{
|
| 12 |
+
private const TABLES = [
|
| 13 |
+
'news',
|
| 14 |
+
'promo',
|
| 15 |
+
'disease',
|
| 16 |
+
'medical_center',
|
| 17 |
+
'site_services',
|
| 18 |
+
];
|
| 19 |
+
|
| 20 |
+
public function getDescription(): string
|
| 21 |
+
{
|
| 22 |
+
return 'Add generated id defaults for content CRUD entities';
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
public function up(Schema $schema): void
|
| 26 |
+
{
|
| 27 |
+
foreach (self::TABLES as $table) {
|
| 28 |
+
$sequence = $table . '_id_seq';
|
| 29 |
+
|
| 30 |
+
$this->addSql(sprintf('CREATE SEQUENCE IF NOT EXISTS %s OWNED BY %s.id', $sequence, $table));
|
| 31 |
+
$this->addSql(sprintf(
|
| 32 |
+
'SELECT setval(\'%s\', COALESCE((SELECT MAX(id) FROM %s), 0) + 1, false)',
|
| 33 |
+
$sequence,
|
| 34 |
+
$table,
|
| 35 |
+
));
|
| 36 |
+
$this->addSql(sprintf(
|
| 37 |
+
'ALTER TABLE %s ALTER COLUMN id SET DEFAULT nextval(\'%s\')',
|
| 38 |
+
$table,
|
| 39 |
+
$sequence,
|
| 40 |
+
));
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
public function down(Schema $schema): void
|
| 45 |
+
{
|
| 46 |
+
foreach (array_reverse(self::TABLES) as $table) {
|
| 47 |
+
$sequence = $table . '_id_seq';
|
| 48 |
+
|
| 49 |
+
$this->addSql(sprintf('ALTER TABLE %s ALTER COLUMN id DROP DEFAULT', $table));
|
| 50 |
+
$this->addSql(sprintf('DROP SEQUENCE IF EXISTS %s', $sequence));
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
}
|
|
@@ -2,54 +2,46 @@
|
|
| 2 |
|
| 3 |
namespace App\Controller;
|
| 4 |
|
|
|
|
| 5 |
use App\Entity\Article;
|
| 6 |
use App\Repository\ArticleRepository;
|
| 7 |
-
use
|
|
|
|
|
|
|
|
|
|
| 8 |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
| 9 |
use Symfony\Component\HttpFoundation\JsonResponse;
|
| 10 |
use Symfony\Component\HttpFoundation\Request;
|
| 11 |
use Symfony\Component\HttpFoundation\Response;
|
| 12 |
use Symfony\Component\Routing\Annotation\Route;
|
| 13 |
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
| 14 |
-
use Symfony\Component\Serializer\SerializerInterface;
|
| 15 |
-
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
| 16 |
-
use Exception;
|
| 17 |
|
| 18 |
#[Route('/article')]
|
| 19 |
final class ArticleController extends AbstractController
|
| 20 |
{
|
|
|
|
|
|
|
|
|
|
| 21 |
public function __construct(
|
| 22 |
-
private
|
| 23 |
-
private
|
| 24 |
-
|
| 25 |
-
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
#[Route('/list', name: 'article_list', methods: ['GET'])]
|
| 28 |
public function list(Request $request, ArticleRepository $repository): JsonResponse
|
| 29 |
{
|
| 30 |
-
$
|
| 31 |
-
$limit = min(100, max(1, (int) $request->query->get('limit', 20)));
|
| 32 |
-
|
| 33 |
-
$filters = [
|
| 34 |
-
'alias' => $request->query->get('alias', ''),
|
| 35 |
-
'active' => $request->query->get('active', ''),
|
| 36 |
-
'regionId' => $request->query->get('regionId', ''),
|
| 37 |
-
];
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
$totalPages = (int) ceil($total / $limit);
|
| 42 |
-
|
| 43 |
-
return $this->json([
|
| 44 |
-
'data' => $articles,
|
| 45 |
-
'meta' => [
|
| 46 |
-
'total' => $total,
|
| 47 |
-
'page' => $page,
|
| 48 |
-
'limit' => $limit,
|
| 49 |
-
'totalPages' => $totalPages,
|
| 50 |
-
],
|
| 51 |
-
], Response::HTTP_OK, [], [
|
| 52 |
-
'groups' => ['article:read']
|
| 53 |
]);
|
| 54 |
}
|
| 55 |
|
|
@@ -60,99 +52,36 @@ final class ArticleController extends AbstractController
|
|
| 60 |
if (!$article) {
|
| 61 |
throw $this->createNotFoundException('Статья не найдена');
|
| 62 |
}
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
]);
|
| 66 |
}
|
| 67 |
|
| 68 |
#[Route('/{id}', name: 'article_show', methods: ['GET'], requirements: ['id' => '\d+'])]
|
| 69 |
public function show(Article $article): JsonResponse
|
| 70 |
{
|
| 71 |
-
return $this->
|
| 72 |
-
'groups' => ['article:read']
|
| 73 |
-
]);
|
| 74 |
}
|
| 75 |
|
| 76 |
#[IsGranted('ROLE_ADMIN')]
|
|
|
|
| 77 |
#[Route('/create', name: 'article_create', methods: ['POST'])]
|
| 78 |
public function create(Request $request): JsonResponse
|
| 79 |
{
|
| 80 |
-
|
| 81 |
-
$article = $this->serializer->deserialize(
|
| 82 |
-
$request->getContent(),
|
| 83 |
-
Article::class,
|
| 84 |
-
'json',
|
| 85 |
-
['groups' => ['article:write']]
|
| 86 |
-
);
|
| 87 |
-
|
| 88 |
-
$errors = $this->validator->validate($article);
|
| 89 |
-
|
| 90 |
-
if (count($errors) > 0) {
|
| 91 |
-
return $this->json($errors, Response::HTTP_BAD_REQUEST);
|
| 92 |
-
}
|
| 93 |
-
|
| 94 |
-
$this->em->persist($article);
|
| 95 |
-
$this->em->flush();
|
| 96 |
-
|
| 97 |
-
return $this->json($article, Response::HTTP_CREATED, [], [
|
| 98 |
-
'groups' => ['article:read']
|
| 99 |
-
]);
|
| 100 |
-
} catch (Exception $e) {
|
| 101 |
-
return new JsonResponse([
|
| 102 |
-
'error' => 'Ошибка при создании статьи',
|
| 103 |
-
'message' => $e->getMessage()
|
| 104 |
-
], Response::HTTP_INTERNAL_SERVER_ERROR);
|
| 105 |
-
}
|
| 106 |
}
|
| 107 |
|
| 108 |
#[IsGranted('ROLE_ADMIN')]
|
|
|
|
| 109 |
#[Route('/{id}', name: 'article_update', methods: ['PUT'], requirements: ['id' => '\d+'])]
|
| 110 |
public function update(Request $request, Article $article): JsonResponse
|
| 111 |
{
|
| 112 |
-
|
| 113 |
-
$this->serializer->deserialize(
|
| 114 |
-
$request->getContent(),
|
| 115 |
-
Article::class,
|
| 116 |
-
'json',
|
| 117 |
-
[
|
| 118 |
-
'groups' => ['article:write'],
|
| 119 |
-
'object_to_populate' => $article
|
| 120 |
-
]
|
| 121 |
-
);
|
| 122 |
-
|
| 123 |
-
$errors = $this->validator->validate($article);
|
| 124 |
-
|
| 125 |
-
if (count($errors) > 0) {
|
| 126 |
-
return $this->json($errors, Response::HTTP_BAD_REQUEST);
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
-
$this->em->flush();
|
| 130 |
-
|
| 131 |
-
return $this->json($article, Response::HTTP_OK, [], [
|
| 132 |
-
'groups' => ['article:read']
|
| 133 |
-
]);
|
| 134 |
-
} catch (Exception $e) {
|
| 135 |
-
return new JsonResponse([
|
| 136 |
-
'error' => 'Ошибка при обновлении статьи',
|
| 137 |
-
'message' => $e->getMessage()
|
| 138 |
-
], Response::HTTP_INTERNAL_SERVER_ERROR);
|
| 139 |
-
}
|
| 140 |
}
|
| 141 |
|
| 142 |
#[IsGranted('ROLE_ADMIN')]
|
| 143 |
#[Route('/{id}', name: 'article_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])]
|
| 144 |
public function delete(Article $article): JsonResponse
|
| 145 |
{
|
| 146 |
-
|
| 147 |
-
$this->em->remove($article);
|
| 148 |
-
$this->em->flush();
|
| 149 |
-
|
| 150 |
-
return new JsonResponse(null, Response::HTTP_NO_CONTENT);
|
| 151 |
-
} catch (Exception $e) {
|
| 152 |
-
return new JsonResponse([
|
| 153 |
-
'error' => 'Ошибка при удалении статьи',
|
| 154 |
-
'message' => $e->getMessage()
|
| 155 |
-
], Response::HTTP_INTERNAL_SERVER_ERROR);
|
| 156 |
-
}
|
| 157 |
}
|
| 158 |
}
|
|
|
|
| 2 |
|
| 3 |
namespace App\Controller;
|
| 4 |
|
| 5 |
+
use App\Dto\Content\ContentFilterDto;
|
| 6 |
use App\Entity\Article;
|
| 7 |
use App\Repository\ArticleRepository;
|
| 8 |
+
use App\Service\Crud\CrudResponder;
|
| 9 |
+
use App\Service\Pagination\Paginator;
|
| 10 |
+
use Nelmio\ApiDocBundle\Attribute\Model;
|
| 11 |
+
use OpenApi\Attributes as OA;
|
| 12 |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
| 13 |
use Symfony\Component\HttpFoundation\JsonResponse;
|
| 14 |
use Symfony\Component\HttpFoundation\Request;
|
| 15 |
use Symfony\Component\HttpFoundation\Response;
|
| 16 |
use Symfony\Component\Routing\Annotation\Route;
|
| 17 |
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
#[Route('/article')]
|
| 20 |
final class ArticleController extends AbstractController
|
| 21 |
{
|
| 22 |
+
private const READ_GROUPS = ['article:read'];
|
| 23 |
+
private const WRITE_GROUPS = ['article:write'];
|
| 24 |
+
|
| 25 |
public function __construct(
|
| 26 |
+
private readonly CrudResponder $crud,
|
| 27 |
+
private readonly Paginator $paginator,
|
| 28 |
+
) {
|
| 29 |
+
}
|
| 30 |
|
| 31 |
+
#[OA\Tag(name: 'Статьи')]
|
| 32 |
+
#[OA\Parameter(name: 'page', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
| 33 |
+
#[OA\Parameter(name: 'limit', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
| 34 |
+
#[OA\Parameter(name: 'regionId', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
| 35 |
+
#[OA\Parameter(name: 'active', in: 'query', schema: new OA\Schema(type: 'boolean'))]
|
| 36 |
+
#[OA\Parameter(name: 'alias', in: 'query', schema: new OA\Schema(type: 'string'))]
|
| 37 |
+
#[OA\Parameter(name: 'search', in: 'query', schema: new OA\Schema(type: 'string'))]
|
| 38 |
#[Route('/list', name: 'article_list', methods: ['GET'])]
|
| 39 |
public function list(Request $request, ArticleRepository $repository): JsonResponse
|
| 40 |
{
|
| 41 |
+
$qb = $repository->createFilteredQueryBuilder(ContentFilterDto::fromRequest($request));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
+
return $this->json($this->paginator->paginateWithLegacyMeta($qb, $request), Response::HTTP_OK, [], [
|
| 44 |
+
'groups' => self::READ_GROUPS,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
]);
|
| 46 |
}
|
| 47 |
|
|
|
|
| 52 |
if (!$article) {
|
| 53 |
throw $this->createNotFoundException('Статья не найдена');
|
| 54 |
}
|
| 55 |
+
|
| 56 |
+
return $this->crud->read($article, self::READ_GROUPS);
|
|
|
|
| 57 |
}
|
| 58 |
|
| 59 |
#[Route('/{id}', name: 'article_show', methods: ['GET'], requirements: ['id' => '\d+'])]
|
| 60 |
public function show(Article $article): JsonResponse
|
| 61 |
{
|
| 62 |
+
return $this->crud->read($article, self::READ_GROUPS);
|
|
|
|
|
|
|
| 63 |
}
|
| 64 |
|
| 65 |
#[IsGranted('ROLE_ADMIN')]
|
| 66 |
+
#[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: Article::class, groups: self::WRITE_GROUPS)))]
|
| 67 |
#[Route('/create', name: 'article_create', methods: ['POST'])]
|
| 68 |
public function create(Request $request): JsonResponse
|
| 69 |
{
|
| 70 |
+
return $this->crud->create($request, Article::class, self::WRITE_GROUPS, self::READ_GROUPS);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
}
|
| 72 |
|
| 73 |
#[IsGranted('ROLE_ADMIN')]
|
| 74 |
+
#[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: Article::class, groups: self::WRITE_GROUPS)))]
|
| 75 |
#[Route('/{id}', name: 'article_update', methods: ['PUT'], requirements: ['id' => '\d+'])]
|
| 76 |
public function update(Request $request, Article $article): JsonResponse
|
| 77 |
{
|
| 78 |
+
return $this->crud->update($request, $article, self::WRITE_GROUPS, self::READ_GROUPS);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
}
|
| 80 |
|
| 81 |
#[IsGranted('ROLE_ADMIN')]
|
| 82 |
#[Route('/{id}', name: 'article_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])]
|
| 83 |
public function delete(Article $article): JsonResponse
|
| 84 |
{
|
| 85 |
+
return $this->crud->delete($article);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
}
|
| 87 |
}
|
|
@@ -2,8 +2,13 @@
|
|
| 2 |
|
| 3 |
namespace App\Controller;
|
| 4 |
|
|
|
|
| 5 |
use App\Entity\Disease;
|
| 6 |
-
use App\
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
| 8 |
use Symfony\Component\HttpFoundation\JsonResponse;
|
| 9 |
use Symfony\Component\HttpFoundation\Request;
|
|
@@ -14,90 +19,57 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
|
| 14 |
#[Route('/disease')]
|
| 15 |
final class DiseaseController extends AbstractController
|
| 16 |
{
|
|
|
|
|
|
|
|
|
|
| 17 |
public function __construct(
|
| 18 |
-
private
|
|
|
|
| 19 |
) {
|
| 20 |
}
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
#[Route('/list', name: 'disease_list', methods: ['GET'])]
|
| 23 |
-
public function list(Request $request): JsonResponse
|
| 24 |
{
|
| 25 |
-
$
|
| 26 |
-
$perPage = min($request->query->getInt('perPage', 100), 500);
|
| 27 |
-
$regionId = $request->query->getInt('regionId', 0) ?: null;
|
| 28 |
-
|
| 29 |
-
$result = $this->diseaseCrud->getPaginatedList($page, $perPage, $regionId);
|
| 30 |
-
$data = $result['data'];
|
| 31 |
-
$total = $result['total'];
|
| 32 |
-
$perPage = $result['per_page'];
|
| 33 |
-
$totalPages = (int) ceil($total / $perPage);
|
| 34 |
|
| 35 |
-
return $this->json([
|
| 36 |
-
'
|
| 37 |
-
'pagination' => [
|
| 38 |
-
'total' => $total,
|
| 39 |
-
'count' => count($data),
|
| 40 |
-
'per_page' => $perPage,
|
| 41 |
-
'current_page' => $result['page'],
|
| 42 |
-
'total_pages' => $totalPages,
|
| 43 |
-
'has_previous_page' => $result['page'] > 1,
|
| 44 |
-
'has_next_page' => $result['page'] < $totalPages,
|
| 45 |
-
],
|
| 46 |
-
], Response::HTTP_OK, [], [
|
| 47 |
-
'groups' => ['disease:read'],
|
| 48 |
]);
|
| 49 |
}
|
| 50 |
|
| 51 |
#[Route('/{id}', name: 'disease_show', methods: ['GET'], requirements: ['id' => '\d+'])]
|
| 52 |
public function show(Disease $disease): JsonResponse
|
| 53 |
{
|
| 54 |
-
return $this->
|
| 55 |
-
'groups' => ['disease:read'],
|
| 56 |
-
]);
|
| 57 |
}
|
| 58 |
|
| 59 |
#[IsGranted('ROLE_ADMIN')]
|
|
|
|
| 60 |
#[Route('/create', name: 'disease_create', methods: ['POST'])]
|
| 61 |
public function create(Request $request): JsonResponse
|
| 62 |
{
|
| 63 |
-
|
| 64 |
-
if (!is_array($data)) {
|
| 65 |
-
return $this->json(['error' => 'Ожидается JSON-объект в теле запроса'], Response::HTTP_BAD_REQUEST);
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
try {
|
| 69 |
-
$disease = $this->diseaseCrud->create($data);
|
| 70 |
-
} catch (\InvalidArgumentException $e) {
|
| 71 |
-
return $this->json(['error' => $e->getMessage()], Response::HTTP_BAD_REQUEST);
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
return $this->json($disease, Response::HTTP_CREATED, [], [
|
| 75 |
-
'groups' => ['disease:read'],
|
| 76 |
-
]);
|
| 77 |
}
|
| 78 |
|
| 79 |
#[IsGranted('ROLE_ADMIN')]
|
|
|
|
| 80 |
#[Route('/{id}', name: 'disease_update', methods: ['PUT'], requirements: ['id' => '\d+'])]
|
| 81 |
-
public function update(
|
| 82 |
{
|
| 83 |
-
|
| 84 |
-
if (!is_array($data)) {
|
| 85 |
-
return $this->json(['error' => 'Ожидается JSON-объект в теле запроса'], Response::HTTP_BAD_REQUEST);
|
| 86 |
-
}
|
| 87 |
-
|
| 88 |
-
$disease = $this->diseaseCrud->update($disease, $data);
|
| 89 |
-
|
| 90 |
-
return $this->json($disease, Response::HTTP_OK, [], [
|
| 91 |
-
'groups' => ['disease:read'],
|
| 92 |
-
]);
|
| 93 |
}
|
| 94 |
|
| 95 |
#[IsGranted('ROLE_ADMIN')]
|
| 96 |
#[Route('/{id}', name: 'disease_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])]
|
| 97 |
public function delete(Disease $disease): JsonResponse
|
| 98 |
{
|
| 99 |
-
$this->
|
| 100 |
-
|
| 101 |
-
return new JsonResponse(null, Response::HTTP_NO_CONTENT);
|
| 102 |
}
|
| 103 |
}
|
|
|
|
| 2 |
|
| 3 |
namespace App\Controller;
|
| 4 |
|
| 5 |
+
use App\Dto\Content\ContentFilterDto;
|
| 6 |
use App\Entity\Disease;
|
| 7 |
+
use App\Repository\DiseaseRepository;
|
| 8 |
+
use App\Service\Crud\CrudResponder;
|
| 9 |
+
use App\Service\Pagination\Paginator;
|
| 10 |
+
use Nelmio\ApiDocBundle\Attribute\Model;
|
| 11 |
+
use OpenApi\Attributes as OA;
|
| 12 |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
| 13 |
use Symfony\Component\HttpFoundation\JsonResponse;
|
| 14 |
use Symfony\Component\HttpFoundation\Request;
|
|
|
|
| 19 |
#[Route('/disease')]
|
| 20 |
final class DiseaseController extends AbstractController
|
| 21 |
{
|
| 22 |
+
private const READ_GROUPS = ['disease:read'];
|
| 23 |
+
private const WRITE_GROUPS = ['disease:write'];
|
| 24 |
+
|
| 25 |
public function __construct(
|
| 26 |
+
private readonly CrudResponder $crud,
|
| 27 |
+
private readonly Paginator $paginator,
|
| 28 |
) {
|
| 29 |
}
|
| 30 |
|
| 31 |
+
#[OA\Tag(name: 'Заболевания')]
|
| 32 |
+
#[OA\Parameter(name: 'page', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
| 33 |
+
#[OA\Parameter(name: 'perPage', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
| 34 |
+
#[OA\Parameter(name: 'regionId', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
| 35 |
+
#[OA\Parameter(name: 'active', in: 'query', schema: new OA\Schema(type: 'boolean'))]
|
| 36 |
+
#[OA\Parameter(name: 'search', in: 'query', schema: new OA\Schema(type: 'string'))]
|
| 37 |
#[Route('/list', name: 'disease_list', methods: ['GET'])]
|
| 38 |
+
public function list(Request $request, DiseaseRepository $repository): JsonResponse
|
| 39 |
{
|
| 40 |
+
$qb = $repository->createFilteredQueryBuilder(ContentFilterDto::fromRequest($request));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
+
return $this->json($this->paginator->paginate($qb, $request), Response::HTTP_OK, [], [
|
| 43 |
+
'groups' => self::READ_GROUPS,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
]);
|
| 45 |
}
|
| 46 |
|
| 47 |
#[Route('/{id}', name: 'disease_show', methods: ['GET'], requirements: ['id' => '\d+'])]
|
| 48 |
public function show(Disease $disease): JsonResponse
|
| 49 |
{
|
| 50 |
+
return $this->crud->read($disease, self::READ_GROUPS);
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
|
| 53 |
#[IsGranted('ROLE_ADMIN')]
|
| 54 |
+
#[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: Disease::class, groups: self::WRITE_GROUPS)))]
|
| 55 |
#[Route('/create', name: 'disease_create', methods: ['POST'])]
|
| 56 |
public function create(Request $request): JsonResponse
|
| 57 |
{
|
| 58 |
+
return $this->crud->create($request, Disease::class, self::WRITE_GROUPS, self::READ_GROUPS);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
}
|
| 60 |
|
| 61 |
#[IsGranted('ROLE_ADMIN')]
|
| 62 |
+
#[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: Disease::class, groups: self::WRITE_GROUPS)))]
|
| 63 |
#[Route('/{id}', name: 'disease_update', methods: ['PUT'], requirements: ['id' => '\d+'])]
|
| 64 |
+
public function update(Request $request, Disease $disease): JsonResponse
|
| 65 |
{
|
| 66 |
+
return $this->crud->update($request, $disease, self::WRITE_GROUPS, self::READ_GROUPS);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
}
|
| 68 |
|
| 69 |
#[IsGranted('ROLE_ADMIN')]
|
| 70 |
#[Route('/{id}', name: 'disease_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])]
|
| 71 |
public function delete(Disease $disease): JsonResponse
|
| 72 |
{
|
| 73 |
+
return $this->crud->delete($disease);
|
|
|
|
|
|
|
| 74 |
}
|
| 75 |
}
|
|
@@ -2,8 +2,13 @@
|
|
| 2 |
|
| 3 |
namespace App\Controller;
|
| 4 |
|
|
|
|
| 5 |
use App\Entity\MedicalCenter;
|
| 6 |
-
use App\
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
| 8 |
use Symfony\Component\HttpFoundation\JsonResponse;
|
| 9 |
use Symfony\Component\HttpFoundation\Request;
|
|
@@ -14,72 +19,57 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
|
| 14 |
#[Route('/medical-center')]
|
| 15 |
final class MedicalCenterController extends AbstractController
|
| 16 |
{
|
|
|
|
|
|
|
|
|
|
| 17 |
public function __construct(
|
| 18 |
-
private
|
|
|
|
| 19 |
) {
|
| 20 |
}
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
#[Route('/list', name: 'medical_center_list', methods: ['GET'])]
|
| 23 |
-
public function list(Request $request): JsonResponse
|
| 24 |
{
|
| 25 |
-
$
|
| 26 |
-
$activeParam = $request->query->get('active');
|
| 27 |
-
$active = $activeParam === null ? true : filter_var($activeParam, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
| 28 |
-
if ($activeParam !== null && $active === null) {
|
| 29 |
-
return $this->json(['error' => 'Параметр active должен быть boolean'], Response::HTTP_BAD_REQUEST);
|
| 30 |
-
}
|
| 31 |
|
| 32 |
-
return $this->json(
|
| 33 |
-
'groups' =>
|
| 34 |
]);
|
| 35 |
}
|
| 36 |
|
| 37 |
#[Route('/{id}', name: 'medical_center_show', methods: ['GET'], requirements: ['id' => '\d+'])]
|
| 38 |
public function show(MedicalCenter $medicalCenter): JsonResponse
|
| 39 |
{
|
| 40 |
-
return $this->
|
| 41 |
-
'groups' => ['medical_center:read'],
|
| 42 |
-
]);
|
| 43 |
}
|
| 44 |
|
| 45 |
#[IsGranted('ROLE_ADMIN')]
|
|
|
|
| 46 |
#[Route('/create', name: 'medical_center_create', methods: ['POST'])]
|
| 47 |
public function create(Request $request): JsonResponse
|
| 48 |
{
|
| 49 |
-
|
| 50 |
-
if (!is_array($data)) {
|
| 51 |
-
return $this->json(['error' => 'Ожидается JSON-объект в теле запроса'], Response::HTTP_BAD_REQUEST);
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
$medicalCenter = $this->medicalCenterCrud->create($data);
|
| 55 |
-
|
| 56 |
-
return $this->json($medicalCenter, Response::HTTP_CREATED, [], [
|
| 57 |
-
'groups' => ['medical_center:read'],
|
| 58 |
-
]);
|
| 59 |
}
|
| 60 |
|
| 61 |
#[IsGranted('ROLE_ADMIN')]
|
|
|
|
| 62 |
#[Route('/{id}', name: 'medical_center_update', methods: ['PUT'], requirements: ['id' => '\d+'])]
|
| 63 |
-
public function update(
|
| 64 |
{
|
| 65 |
-
|
| 66 |
-
if (!is_array($data)) {
|
| 67 |
-
return $this->json(['error' => 'Ожидается JSON-объект в теле запроса'], Response::HTTP_BAD_REQUEST);
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
$medicalCenter = $this->medicalCenterCrud->update($medicalCenter, $data);
|
| 71 |
-
|
| 72 |
-
return $this->json($medicalCenter, Response::HTTP_OK, [], [
|
| 73 |
-
'groups' => ['medical_center:read'],
|
| 74 |
-
]);
|
| 75 |
}
|
| 76 |
|
| 77 |
#[IsGranted('ROLE_ADMIN')]
|
| 78 |
#[Route('/{id}', name: 'medical_center_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])]
|
| 79 |
public function delete(MedicalCenter $medicalCenter): JsonResponse
|
| 80 |
{
|
| 81 |
-
$this->
|
| 82 |
-
|
| 83 |
-
return new JsonResponse(null, Response::HTTP_NO_CONTENT);
|
| 84 |
}
|
| 85 |
}
|
|
|
|
| 2 |
|
| 3 |
namespace App\Controller;
|
| 4 |
|
| 5 |
+
use App\Dto\Content\ContentFilterDto;
|
| 6 |
use App\Entity\MedicalCenter;
|
| 7 |
+
use App\Repository\MedicalCenterRepository;
|
| 8 |
+
use App\Service\Crud\CrudResponder;
|
| 9 |
+
use App\Service\Pagination\Paginator;
|
| 10 |
+
use Nelmio\ApiDocBundle\Attribute\Model;
|
| 11 |
+
use OpenApi\Attributes as OA;
|
| 12 |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
| 13 |
use Symfony\Component\HttpFoundation\JsonResponse;
|
| 14 |
use Symfony\Component\HttpFoundation\Request;
|
|
|
|
| 19 |
#[Route('/medical-center')]
|
| 20 |
final class MedicalCenterController extends AbstractController
|
| 21 |
{
|
| 22 |
+
private const READ_GROUPS = ['medical_center:read'];
|
| 23 |
+
private const WRITE_GROUPS = ['medical_center:write'];
|
| 24 |
+
|
| 25 |
public function __construct(
|
| 26 |
+
private readonly CrudResponder $crud,
|
| 27 |
+
private readonly Paginator $paginator,
|
| 28 |
) {
|
| 29 |
}
|
| 30 |
|
| 31 |
+
#[OA\Tag(name: 'Центры')]
|
| 32 |
+
#[OA\Parameter(name: 'page', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
| 33 |
+
#[OA\Parameter(name: 'perPage', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
| 34 |
+
#[OA\Parameter(name: 'regionId', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
| 35 |
+
#[OA\Parameter(name: 'active', description: 'Если не передан — фильтр active=true (как в старом API).', in: 'query', schema: new OA\Schema(type: 'boolean'))]
|
| 36 |
+
#[OA\Parameter(name: 'search', in: 'query', schema: new OA\Schema(type: 'string'))]
|
| 37 |
#[Route('/list', name: 'medical_center_list', methods: ['GET'])]
|
| 38 |
+
public function list(Request $request, MedicalCenterRepository $repository): JsonResponse
|
| 39 |
{
|
| 40 |
+
$qb = $repository->createFilteredQueryBuilder(ContentFilterDto::fromRequest($request, true));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
+
return $this->json($this->paginator->paginate($qb, $request), Response::HTTP_OK, [], [
|
| 43 |
+
'groups' => self::READ_GROUPS,
|
| 44 |
]);
|
| 45 |
}
|
| 46 |
|
| 47 |
#[Route('/{id}', name: 'medical_center_show', methods: ['GET'], requirements: ['id' => '\d+'])]
|
| 48 |
public function show(MedicalCenter $medicalCenter): JsonResponse
|
| 49 |
{
|
| 50 |
+
return $this->crud->read($medicalCenter, self::READ_GROUPS);
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
|
| 53 |
#[IsGranted('ROLE_ADMIN')]
|
| 54 |
+
#[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: MedicalCenter::class, groups: self::WRITE_GROUPS)))]
|
| 55 |
#[Route('/create', name: 'medical_center_create', methods: ['POST'])]
|
| 56 |
public function create(Request $request): JsonResponse
|
| 57 |
{
|
| 58 |
+
return $this->crud->create($request, MedicalCenter::class, self::WRITE_GROUPS, self::READ_GROUPS);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
}
|
| 60 |
|
| 61 |
#[IsGranted('ROLE_ADMIN')]
|
| 62 |
+
#[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: MedicalCenter::class, groups: self::WRITE_GROUPS)))]
|
| 63 |
#[Route('/{id}', name: 'medical_center_update', methods: ['PUT'], requirements: ['id' => '\d+'])]
|
| 64 |
+
public function update(Request $request, MedicalCenter $medicalCenter): JsonResponse
|
| 65 |
{
|
| 66 |
+
return $this->crud->update($request, $medicalCenter, self::WRITE_GROUPS, self::READ_GROUPS);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
}
|
| 68 |
|
| 69 |
#[IsGranted('ROLE_ADMIN')]
|
| 70 |
#[Route('/{id}', name: 'medical_center_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])]
|
| 71 |
public function delete(MedicalCenter $medicalCenter): JsonResponse
|
| 72 |
{
|
| 73 |
+
return $this->crud->delete($medicalCenter);
|
|
|
|
|
|
|
| 74 |
}
|
| 75 |
}
|
|
@@ -2,8 +2,13 @@
|
|
| 2 |
|
| 3 |
namespace App\Controller;
|
| 4 |
|
|
|
|
| 5 |
use App\Entity\News;
|
| 6 |
-
use App\
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
| 8 |
use Symfony\Component\HttpFoundation\JsonResponse;
|
| 9 |
use Symfony\Component\HttpFoundation\Request;
|
|
@@ -14,72 +19,57 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
|
| 14 |
#[Route('/news')]
|
| 15 |
final class NewsController extends AbstractController
|
| 16 |
{
|
|
|
|
|
|
|
|
|
|
| 17 |
public function __construct(
|
| 18 |
-
private
|
|
|
|
| 19 |
) {
|
| 20 |
}
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
#[Route('/list', name: 'news_list', methods: ['GET'])]
|
| 23 |
-
public function list(Request $request): JsonResponse
|
| 24 |
{
|
| 25 |
-
$
|
| 26 |
-
$activeParam = $request->query->get('active');
|
| 27 |
-
$active = $activeParam === null ? true : filter_var($activeParam, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
| 28 |
-
if ($activeParam !== null && $active === null) {
|
| 29 |
-
return $this->json(['error' => 'Параметр active должен быть boolean'], Response::HTTP_BAD_REQUEST);
|
| 30 |
-
}
|
| 31 |
|
| 32 |
-
return $this->json(
|
| 33 |
-
'groups' =>
|
| 34 |
]);
|
| 35 |
}
|
| 36 |
|
| 37 |
#[Route('/{id}', name: 'news_show', methods: ['GET'], requirements: ['id' => '\d+'])]
|
| 38 |
public function show(News $news): JsonResponse
|
| 39 |
{
|
| 40 |
-
return $this->
|
| 41 |
-
'groups' => ['news:read'],
|
| 42 |
-
]);
|
| 43 |
}
|
| 44 |
|
| 45 |
#[IsGranted('ROLE_ADMIN')]
|
|
|
|
| 46 |
#[Route('/create', name: 'news_create', methods: ['POST'])]
|
| 47 |
public function create(Request $request): JsonResponse
|
| 48 |
{
|
| 49 |
-
|
| 50 |
-
if (!is_array($data)) {
|
| 51 |
-
return $this->json(['error' => 'Ожидается JSON-объект в теле запроса'], Response::HTTP_BAD_REQUEST);
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
$news = $this->newsCrud->create($data);
|
| 55 |
-
|
| 56 |
-
return $this->json($news, Response::HTTP_CREATED, [], [
|
| 57 |
-
'groups' => ['news:read'],
|
| 58 |
-
]);
|
| 59 |
}
|
| 60 |
|
| 61 |
#[IsGranted('ROLE_ADMIN')]
|
|
|
|
| 62 |
#[Route('/{id}', name: 'news_update', methods: ['PUT'], requirements: ['id' => '\d+'])]
|
| 63 |
-
public function update(
|
| 64 |
{
|
| 65 |
-
|
| 66 |
-
if (!is_array($data)) {
|
| 67 |
-
return $this->json(['error' => 'Ожидается JSON-объект в теле запроса'], Response::HTTP_BAD_REQUEST);
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
$news = $this->newsCrud->update($news, $data);
|
| 71 |
-
|
| 72 |
-
return $this->json($news, Response::HTTP_OK, [], [
|
| 73 |
-
'groups' => ['news:read'],
|
| 74 |
-
]);
|
| 75 |
}
|
| 76 |
|
| 77 |
#[IsGranted('ROLE_ADMIN')]
|
| 78 |
#[Route('/{id}', name: 'news_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])]
|
| 79 |
public function delete(News $news): JsonResponse
|
| 80 |
{
|
| 81 |
-
$this->
|
| 82 |
-
|
| 83 |
-
return new JsonResponse(null, Response::HTTP_NO_CONTENT);
|
| 84 |
}
|
| 85 |
}
|
|
|
|
| 2 |
|
| 3 |
namespace App\Controller;
|
| 4 |
|
| 5 |
+
use App\Dto\Content\ContentFilterDto;
|
| 6 |
use App\Entity\News;
|
| 7 |
+
use App\Repository\NewsRepository;
|
| 8 |
+
use App\Service\Crud\CrudResponder;
|
| 9 |
+
use App\Service\Pagination\Paginator;
|
| 10 |
+
use Nelmio\ApiDocBundle\Attribute\Model;
|
| 11 |
+
use OpenApi\Attributes as OA;
|
| 12 |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
| 13 |
use Symfony\Component\HttpFoundation\JsonResponse;
|
| 14 |
use Symfony\Component\HttpFoundation\Request;
|
|
|
|
| 19 |
#[Route('/news')]
|
| 20 |
final class NewsController extends AbstractController
|
| 21 |
{
|
| 22 |
+
private const READ_GROUPS = ['news:read'];
|
| 23 |
+
private const WRITE_GROUPS = ['news:write'];
|
| 24 |
+
|
| 25 |
public function __construct(
|
| 26 |
+
private readonly CrudResponder $crud,
|
| 27 |
+
private readonly Paginator $paginator,
|
| 28 |
) {
|
| 29 |
}
|
| 30 |
|
| 31 |
+
#[OA\Tag(name: 'Новости')]
|
| 32 |
+
#[OA\Parameter(name: 'page', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
| 33 |
+
#[OA\Parameter(name: 'perPage', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
| 34 |
+
#[OA\Parameter(name: 'regionId', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
| 35 |
+
#[OA\Parameter(name: 'active', description: 'Если не передан — фильтр active=true (как в старом API).', in: 'query', schema: new OA\Schema(type: 'boolean'))]
|
| 36 |
+
#[OA\Parameter(name: 'search', in: 'query', schema: new OA\Schema(type: 'string'))]
|
| 37 |
#[Route('/list', name: 'news_list', methods: ['GET'])]
|
| 38 |
+
public function list(Request $request, NewsRepository $repository): JsonResponse
|
| 39 |
{
|
| 40 |
+
$qb = $repository->createFilteredQueryBuilder(ContentFilterDto::fromRequest($request, true));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
+
return $this->json($this->paginator->paginate($qb, $request), Response::HTTP_OK, [], [
|
| 43 |
+
'groups' => self::READ_GROUPS,
|
| 44 |
]);
|
| 45 |
}
|
| 46 |
|
| 47 |
#[Route('/{id}', name: 'news_show', methods: ['GET'], requirements: ['id' => '\d+'])]
|
| 48 |
public function show(News $news): JsonResponse
|
| 49 |
{
|
| 50 |
+
return $this->crud->read($news, self::READ_GROUPS);
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
|
| 53 |
#[IsGranted('ROLE_ADMIN')]
|
| 54 |
+
#[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: News::class, groups: self::WRITE_GROUPS)))]
|
| 55 |
#[Route('/create', name: 'news_create', methods: ['POST'])]
|
| 56 |
public function create(Request $request): JsonResponse
|
| 57 |
{
|
| 58 |
+
return $this->crud->create($request, News::class, self::WRITE_GROUPS, self::READ_GROUPS);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
}
|
| 60 |
|
| 61 |
#[IsGranted('ROLE_ADMIN')]
|
| 62 |
+
#[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: News::class, groups: self::WRITE_GROUPS)))]
|
| 63 |
#[Route('/{id}', name: 'news_update', methods: ['PUT'], requirements: ['id' => '\d+'])]
|
| 64 |
+
public function update(Request $request, News $news): JsonResponse
|
| 65 |
{
|
| 66 |
+
return $this->crud->update($request, $news, self::WRITE_GROUPS, self::READ_GROUPS);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
}
|
| 68 |
|
| 69 |
#[IsGranted('ROLE_ADMIN')]
|
| 70 |
#[Route('/{id}', name: 'news_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])]
|
| 71 |
public function delete(News $news): JsonResponse
|
| 72 |
{
|
| 73 |
+
return $this->crud->delete($news);
|
|
|
|
|
|
|
| 74 |
}
|
| 75 |
}
|
|
@@ -2,8 +2,13 @@
|
|
| 2 |
|
| 3 |
namespace App\Controller;
|
| 4 |
|
|
|
|
| 5 |
use App\Entity\Promo;
|
| 6 |
-
use App\
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
| 8 |
use Symfony\Component\HttpFoundation\JsonResponse;
|
| 9 |
use Symfony\Component\HttpFoundation\Request;
|
|
@@ -14,72 +19,57 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
|
| 14 |
#[Route('/promo')]
|
| 15 |
final class PromoController extends AbstractController
|
| 16 |
{
|
|
|
|
|
|
|
|
|
|
| 17 |
public function __construct(
|
| 18 |
-
private
|
|
|
|
| 19 |
) {
|
| 20 |
}
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
#[Route('/list', name: 'promo_list', methods: ['GET'])]
|
| 23 |
-
public function list(Request $request): JsonResponse
|
| 24 |
{
|
| 25 |
-
$
|
| 26 |
-
$activeParam = $request->query->get('active');
|
| 27 |
-
$active = $activeParam === null ? true : filter_var($activeParam, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
| 28 |
-
if ($activeParam !== null && $active === null) {
|
| 29 |
-
return $this->json(['error' => 'Параметр active должен быть boolean'], Response::HTTP_BAD_REQUEST);
|
| 30 |
-
}
|
| 31 |
|
| 32 |
-
return $this->json(
|
| 33 |
-
'groups' =>
|
| 34 |
]);
|
| 35 |
}
|
| 36 |
|
| 37 |
#[Route('/{id}', name: 'promo_show', methods: ['GET'], requirements: ['id' => '\d+'])]
|
| 38 |
public function show(Promo $promo): JsonResponse
|
| 39 |
{
|
| 40 |
-
return $this->
|
| 41 |
-
'groups' => ['promo:read'],
|
| 42 |
-
]);
|
| 43 |
}
|
| 44 |
|
| 45 |
#[IsGranted('ROLE_ADMIN')]
|
|
|
|
| 46 |
#[Route('/create', name: 'promo_create', methods: ['POST'])]
|
| 47 |
public function create(Request $request): JsonResponse
|
| 48 |
{
|
| 49 |
-
|
| 50 |
-
if (!is_array($data)) {
|
| 51 |
-
return $this->json(['error' => 'Ожидается JSON-объект в теле запроса'], Response::HTTP_BAD_REQUEST);
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
$promo = $this->promoCrud->create($data);
|
| 55 |
-
|
| 56 |
-
return $this->json($promo, Response::HTTP_CREATED, [], [
|
| 57 |
-
'groups' => ['promo:read'],
|
| 58 |
-
]);
|
| 59 |
}
|
| 60 |
|
| 61 |
#[IsGranted('ROLE_ADMIN')]
|
|
|
|
| 62 |
#[Route('/{id}', name: 'promo_update', methods: ['PUT'], requirements: ['id' => '\d+'])]
|
| 63 |
-
public function update(
|
| 64 |
{
|
| 65 |
-
|
| 66 |
-
if (!is_array($data)) {
|
| 67 |
-
return $this->json(['error' => 'Ожидается JSON-объект в теле запроса'], Response::HTTP_BAD_REQUEST);
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
$promo = $this->promoCrud->update($promo, $data);
|
| 71 |
-
|
| 72 |
-
return $this->json($promo, Response::HTTP_OK, [], [
|
| 73 |
-
'groups' => ['promo:read'],
|
| 74 |
-
]);
|
| 75 |
}
|
| 76 |
|
| 77 |
#[IsGranted('ROLE_ADMIN')]
|
| 78 |
#[Route('/{id}', name: 'promo_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])]
|
| 79 |
public function delete(Promo $promo): JsonResponse
|
| 80 |
{
|
| 81 |
-
$this->
|
| 82 |
-
|
| 83 |
-
return new JsonResponse(null, Response::HTTP_NO_CONTENT);
|
| 84 |
}
|
| 85 |
}
|
|
|
|
| 2 |
|
| 3 |
namespace App\Controller;
|
| 4 |
|
| 5 |
+
use App\Dto\Content\ContentFilterDto;
|
| 6 |
use App\Entity\Promo;
|
| 7 |
+
use App\Repository\PromoRepository;
|
| 8 |
+
use App\Service\Crud\CrudResponder;
|
| 9 |
+
use App\Service\Pagination\Paginator;
|
| 10 |
+
use Nelmio\ApiDocBundle\Attribute\Model;
|
| 11 |
+
use OpenApi\Attributes as OA;
|
| 12 |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
| 13 |
use Symfony\Component\HttpFoundation\JsonResponse;
|
| 14 |
use Symfony\Component\HttpFoundation\Request;
|
|
|
|
| 19 |
#[Route('/promo')]
|
| 20 |
final class PromoController extends AbstractController
|
| 21 |
{
|
| 22 |
+
private const READ_GROUPS = ['promo:read'];
|
| 23 |
+
private const WRITE_GROUPS = ['promo:write'];
|
| 24 |
+
|
| 25 |
public function __construct(
|
| 26 |
+
private readonly CrudResponder $crud,
|
| 27 |
+
private readonly Paginator $paginator,
|
| 28 |
) {
|
| 29 |
}
|
| 30 |
|
| 31 |
+
#[OA\Tag(name: 'Акции')]
|
| 32 |
+
#[OA\Parameter(name: 'page', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
| 33 |
+
#[OA\Parameter(name: 'perPage', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
| 34 |
+
#[OA\Parameter(name: 'regionId', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
| 35 |
+
#[OA\Parameter(name: 'active', description: 'Если не передан — фильтр active=true (как в старом API).', in: 'query', schema: new OA\Schema(type: 'boolean'))]
|
| 36 |
+
#[OA\Parameter(name: 'search', in: 'query', schema: new OA\Schema(type: 'string'))]
|
| 37 |
#[Route('/list', name: 'promo_list', methods: ['GET'])]
|
| 38 |
+
public function list(Request $request, PromoRepository $repository): JsonResponse
|
| 39 |
{
|
| 40 |
+
$qb = $repository->createFilteredQueryBuilder(ContentFilterDto::fromRequest($request, true));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
+
return $this->json($this->paginator->paginate($qb, $request), Response::HTTP_OK, [], [
|
| 43 |
+
'groups' => self::READ_GROUPS,
|
| 44 |
]);
|
| 45 |
}
|
| 46 |
|
| 47 |
#[Route('/{id}', name: 'promo_show', methods: ['GET'], requirements: ['id' => '\d+'])]
|
| 48 |
public function show(Promo $promo): JsonResponse
|
| 49 |
{
|
| 50 |
+
return $this->crud->read($promo, self::READ_GROUPS);
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
|
| 53 |
#[IsGranted('ROLE_ADMIN')]
|
| 54 |
+
#[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: Promo::class, groups: self::WRITE_GROUPS)))]
|
| 55 |
#[Route('/create', name: 'promo_create', methods: ['POST'])]
|
| 56 |
public function create(Request $request): JsonResponse
|
| 57 |
{
|
| 58 |
+
return $this->crud->create($request, Promo::class, self::WRITE_GROUPS, self::READ_GROUPS);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
}
|
| 60 |
|
| 61 |
#[IsGranted('ROLE_ADMIN')]
|
| 62 |
+
#[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: Promo::class, groups: self::WRITE_GROUPS)))]
|
| 63 |
#[Route('/{id}', name: 'promo_update', methods: ['PUT'], requirements: ['id' => '\d+'])]
|
| 64 |
+
public function update(Request $request, Promo $promo): JsonResponse
|
| 65 |
{
|
| 66 |
+
return $this->crud->update($request, $promo, self::WRITE_GROUPS, self::READ_GROUPS);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
}
|
| 68 |
|
| 69 |
#[IsGranted('ROLE_ADMIN')]
|
| 70 |
#[Route('/{id}', name: 'promo_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])]
|
| 71 |
public function delete(Promo $promo): JsonResponse
|
| 72 |
{
|
| 73 |
+
return $this->crud->delete($promo);
|
|
|
|
|
|
|
| 74 |
}
|
| 75 |
}
|
|
@@ -2,8 +2,13 @@
|
|
| 2 |
|
| 3 |
namespace App\Controller;
|
| 4 |
|
|
|
|
| 5 |
use App\Entity\SiteService;
|
| 6 |
-
use App\
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
| 8 |
use Symfony\Component\HttpFoundation\JsonResponse;
|
| 9 |
use Symfony\Component\HttpFoundation\Request;
|
|
@@ -14,91 +19,57 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
|
| 14 |
#[Route('/site-services')]
|
| 15 |
final class SiteServiceController extends AbstractController
|
| 16 |
{
|
|
|
|
|
|
|
|
|
|
| 17 |
public function __construct(
|
| 18 |
-
private
|
|
|
|
| 19 |
) {
|
| 20 |
}
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
#[Route('/list', name: 'site_service_list', methods: ['GET'])]
|
| 23 |
-
public function list(Request $request): JsonResponse
|
| 24 |
{
|
| 25 |
-
$
|
| 26 |
-
$perPage = min($request->query->getInt('perPage', 50), 500);
|
| 27 |
-
$regionId = $request->query->getInt('regionId', 0) ?: null;
|
| 28 |
-
$activeParam = $request->query->get('active');
|
| 29 |
-
$active = $activeParam === null ? true : filter_var($activeParam, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
| 30 |
-
if ($activeParam !== null && $active === null) {
|
| 31 |
-
return $this->json(['error' => 'Параметр active должен быть boolean'], Response::HTTP_BAD_REQUEST);
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
$result = $this->siteServiceCrud->getPaginatedList($page, $perPage, $regionId, $active);
|
| 35 |
-
$data = $result['data'];
|
| 36 |
-
$total = $result['total'];
|
| 37 |
-
$perPage = $result['per_page'];
|
| 38 |
-
$totalPages = (int) ceil($total / $perPage);
|
| 39 |
|
| 40 |
-
return $this->json([
|
| 41 |
-
'
|
| 42 |
-
'pagination' => [
|
| 43 |
-
'total' => $total,
|
| 44 |
-
'count' => count($data),
|
| 45 |
-
'per_page' => $perPage,
|
| 46 |
-
'current_page' => $result['page'],
|
| 47 |
-
'total_pages' => $totalPages,
|
| 48 |
-
'has_previous_page' => $result['page'] > 1,
|
| 49 |
-
'has_next_page' => $result['page'] < $totalPages,
|
| 50 |
-
],
|
| 51 |
-
], Response::HTTP_OK, [], [
|
| 52 |
-
'groups' => ['site_service:read'],
|
| 53 |
]);
|
| 54 |
}
|
| 55 |
|
| 56 |
#[Route('/{id}', name: 'site_service_show', methods: ['GET'], requirements: ['id' => '\d+'])]
|
| 57 |
public function show(SiteService $siteService): JsonResponse
|
| 58 |
{
|
| 59 |
-
return $this->
|
| 60 |
-
'groups' => ['site_service:read'],
|
| 61 |
-
]);
|
| 62 |
}
|
| 63 |
|
| 64 |
#[IsGranted('ROLE_ADMIN')]
|
|
|
|
| 65 |
#[Route('/create', name: 'site_service_create', methods: ['POST'])]
|
| 66 |
public function create(Request $request): JsonResponse
|
| 67 |
{
|
| 68 |
-
|
| 69 |
-
if (!is_array($data)) {
|
| 70 |
-
return $this->json(['error' => 'Ожидается JSON-объект в теле запроса'], Response::HTTP_BAD_REQUEST);
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
$siteService = $this->siteServiceCrud->create($data);
|
| 74 |
-
|
| 75 |
-
return $this->json($siteService, Response::HTTP_CREATED, [], [
|
| 76 |
-
'groups' => ['site_service:read'],
|
| 77 |
-
]);
|
| 78 |
}
|
| 79 |
|
| 80 |
#[IsGranted('ROLE_ADMIN')]
|
|
|
|
| 81 |
#[Route('/{id}', name: 'site_service_update', methods: ['PUT'], requirements: ['id' => '\d+'])]
|
| 82 |
-
public function update(
|
| 83 |
{
|
| 84 |
-
|
| 85 |
-
if (!is_array($data)) {
|
| 86 |
-
return $this->json(['error' => 'Ожидается JSON-объект в теле запроса'], Response::HTTP_BAD_REQUEST);
|
| 87 |
-
}
|
| 88 |
-
|
| 89 |
-
$siteService = $this->siteServiceCrud->update($siteService, $data);
|
| 90 |
-
|
| 91 |
-
return $this->json($siteService, Response::HTTP_OK, [], [
|
| 92 |
-
'groups' => ['site_service:read'],
|
| 93 |
-
]);
|
| 94 |
}
|
| 95 |
|
| 96 |
#[IsGranted('ROLE_ADMIN')]
|
| 97 |
#[Route('/{id}', name: 'site_service_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])]
|
| 98 |
public function delete(SiteService $siteService): JsonResponse
|
| 99 |
{
|
| 100 |
-
$this->
|
| 101 |
-
|
| 102 |
-
return new JsonResponse(null, Response::HTTP_NO_CONTENT);
|
| 103 |
}
|
| 104 |
}
|
|
|
|
| 2 |
|
| 3 |
namespace App\Controller;
|
| 4 |
|
| 5 |
+
use App\Dto\Content\ContentFilterDto;
|
| 6 |
use App\Entity\SiteService;
|
| 7 |
+
use App\Repository\SiteServiceRepository;
|
| 8 |
+
use App\Service\Crud\CrudResponder;
|
| 9 |
+
use App\Service\Pagination\Paginator;
|
| 10 |
+
use Nelmio\ApiDocBundle\Attribute\Model;
|
| 11 |
+
use OpenApi\Attributes as OA;
|
| 12 |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
| 13 |
use Symfony\Component\HttpFoundation\JsonResponse;
|
| 14 |
use Symfony\Component\HttpFoundation\Request;
|
|
|
|
| 19 |
#[Route('/site-services')]
|
| 20 |
final class SiteServiceController extends AbstractController
|
| 21 |
{
|
| 22 |
+
private const READ_GROUPS = ['site_service:read'];
|
| 23 |
+
private const WRITE_GROUPS = ['site_service:write'];
|
| 24 |
+
|
| 25 |
public function __construct(
|
| 26 |
+
private readonly CrudResponder $crud,
|
| 27 |
+
private readonly Paginator $paginator,
|
| 28 |
) {
|
| 29 |
}
|
| 30 |
|
| 31 |
+
#[OA\Tag(name: 'Услуги')]
|
| 32 |
+
#[OA\Parameter(name: 'page', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
| 33 |
+
#[OA\Parameter(name: 'perPage', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
| 34 |
+
#[OA\Parameter(name: 'regionId', in: 'query', schema: new OA\Schema(type: 'integer'))]
|
| 35 |
+
#[OA\Parameter(name: 'active', description: 'Если не передан — фильтр active=true (как в старом API).', in: 'query', schema: new OA\Schema(type: 'boolean'))]
|
| 36 |
+
#[OA\Parameter(name: 'search', in: 'query', schema: new OA\Schema(type: 'string'))]
|
| 37 |
#[Route('/list', name: 'site_service_list', methods: ['GET'])]
|
| 38 |
+
public function list(Request $request, SiteServiceRepository $repository): JsonResponse
|
| 39 |
{
|
| 40 |
+
$qb = $repository->createFilteredQueryBuilder(ContentFilterDto::fromRequest($request, true));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
+
return $this->json($this->paginator->paginate($qb, $request), Response::HTTP_OK, [], [
|
| 43 |
+
'groups' => self::READ_GROUPS,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
]);
|
| 45 |
}
|
| 46 |
|
| 47 |
#[Route('/{id}', name: 'site_service_show', methods: ['GET'], requirements: ['id' => '\d+'])]
|
| 48 |
public function show(SiteService $siteService): JsonResponse
|
| 49 |
{
|
| 50 |
+
return $this->crud->read($siteService, self::READ_GROUPS);
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
|
| 53 |
#[IsGranted('ROLE_ADMIN')]
|
| 54 |
+
#[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: SiteService::class, groups: self::WRITE_GROUPS)))]
|
| 55 |
#[Route('/create', name: 'site_service_create', methods: ['POST'])]
|
| 56 |
public function create(Request $request): JsonResponse
|
| 57 |
{
|
| 58 |
+
return $this->crud->create($request, SiteService::class, self::WRITE_GROUPS, self::READ_GROUPS);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
}
|
| 60 |
|
| 61 |
#[IsGranted('ROLE_ADMIN')]
|
| 62 |
+
#[OA\RequestBody(content: new OA\JsonContent(ref: new Model(type: SiteService::class, groups: self::WRITE_GROUPS)))]
|
| 63 |
#[Route('/{id}', name: 'site_service_update', methods: ['PUT'], requirements: ['id' => '\d+'])]
|
| 64 |
+
public function update(Request $request, SiteService $siteService): JsonResponse
|
| 65 |
{
|
| 66 |
+
return $this->crud->update($request, $siteService, self::WRITE_GROUPS, self::READ_GROUPS);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
}
|
| 68 |
|
| 69 |
#[IsGranted('ROLE_ADMIN')]
|
| 70 |
#[Route('/{id}', name: 'site_service_delete', methods: ['DELETE'], requirements: ['id' => '\d+'])]
|
| 71 |
public function delete(SiteService $siteService): JsonResponse
|
| 72 |
{
|
| 73 |
+
return $this->crud->delete($siteService);
|
|
|
|
|
|
|
| 74 |
}
|
| 75 |
}
|
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
declare(strict_types=1);
|
| 4 |
+
|
| 5 |
+
namespace App\Dto\Content;
|
| 6 |
+
|
| 7 |
+
use Symfony\Component\HttpFoundation\Request;
|
| 8 |
+
|
| 9 |
+
final readonly class ContentFilterDto
|
| 10 |
+
{
|
| 11 |
+
public function __construct(
|
| 12 |
+
public ?int $regionId = null,
|
| 13 |
+
public ?bool $active = null,
|
| 14 |
+
public ?string $alias = null,
|
| 15 |
+
public ?string $search = null,
|
| 16 |
+
) {
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
/**
|
| 20 |
+
* @param ?bool $defaultActive если задан (например, true), подставляется,
|
| 21 |
+
* когда query-параметр `active` отсутствует или пустой.
|
| 22 |
+
* Легаси: в старых list-эндпоинтах News/Promo/MedicalCenter/SiteService
|
| 23 |
+
* при отсутствии `active` подразумевалось active = true.
|
| 24 |
+
*/
|
| 25 |
+
public static function fromRequest(Request $request, ?bool $defaultActive = null): self
|
| 26 |
+
{
|
| 27 |
+
$active = self::nullableBool($request->query->get('active'));
|
| 28 |
+
|
| 29 |
+
return new self(
|
| 30 |
+
regionId: self::positiveInt($request->query->get('regionId', $request->query->get('region_id'))),
|
| 31 |
+
active: $active ?? $defaultActive,
|
| 32 |
+
alias: self::nonEmptyString($request->query->get('alias')),
|
| 33 |
+
search: self::nonEmptyString($request->query->get('search', $request->query->get('q'))),
|
| 34 |
+
);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/**
|
| 38 |
+
* Symfony QueryBag может отдать массив при ?regionId[]=… — не передаём его в is_numeric (TypeError в PHP 8).
|
| 39 |
+
*/
|
| 40 |
+
private static function positiveInt(mixed $value): ?int
|
| 41 |
+
{
|
| 42 |
+
if ($value === null || $value === '' || !is_scalar($value) || !is_numeric($value)) {
|
| 43 |
+
return null;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
$value = (int) $value;
|
| 47 |
+
|
| 48 |
+
return $value > 0 ? $value : null;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/**
|
| 52 |
+
* При ?active[]=… query->get вернёт массив — отбрасываем без вызова filter_var по нему.
|
| 53 |
+
*/
|
| 54 |
+
private static function nullableBool(mixed $value): ?bool
|
| 55 |
+
{
|
| 56 |
+
if ($value === null || $value === '') {
|
| 57 |
+
return null;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
if (!is_scalar($value)) {
|
| 61 |
+
return null;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
if (is_bool($value)) {
|
| 65 |
+
return $value;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
private static function nonEmptyString(mixed $value): ?string
|
| 72 |
+
{
|
| 73 |
+
if (!is_string($value)) {
|
| 74 |
+
return null;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
$value = trim($value);
|
| 78 |
+
|
| 79 |
+
return $value !== '' ? $value : null;
|
| 80 |
+
}
|
| 81 |
+
}
|
|
@@ -2,6 +2,7 @@
|
|
| 2 |
|
| 3 |
namespace App\Entity;
|
| 4 |
|
|
|
|
| 5 |
use App\Repository\ArticleRepository;
|
| 6 |
use Doctrine\DBAL\Types\Types;
|
| 7 |
use Doctrine\ORM\Mapping as ORM;
|
|
@@ -12,8 +13,11 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|
| 12 |
#[ORM\Table(name: 'article')]
|
| 13 |
#[ORM\Index(name: 'idx_article_region_id', columns: ['region_id'])]
|
| 14 |
#[ORM\Index(name: 'idx_article_active', columns: ['active'])]
|
|
|
|
| 15 |
class Article
|
| 16 |
{
|
|
|
|
|
|
|
| 17 |
#[Groups(['article:read'])]
|
| 18 |
#[ORM\Id]
|
| 19 |
#[ORM\GeneratedValue(strategy: "IDENTITY")]
|
|
@@ -56,7 +60,7 @@ class Article
|
|
| 56 |
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
| 57 |
private ?string $content = null;
|
| 58 |
|
| 59 |
-
#[Groups(['article:read'
|
| 60 |
#[ORM\Column(name: 'update_at', type: Types::DATETIME_MUTABLE, nullable: true)]
|
| 61 |
private ?\DateTimeInterface $updateAt = null;
|
| 62 |
|
|
|
|
| 2 |
|
| 3 |
namespace App\Entity;
|
| 4 |
|
| 5 |
+
use App\Entity\Behavior\UpdateTimestampTrait;
|
| 6 |
use App\Repository\ArticleRepository;
|
| 7 |
use Doctrine\DBAL\Types\Types;
|
| 8 |
use Doctrine\ORM\Mapping as ORM;
|
|
|
|
| 13 |
#[ORM\Table(name: 'article')]
|
| 14 |
#[ORM\Index(name: 'idx_article_region_id', columns: ['region_id'])]
|
| 15 |
#[ORM\Index(name: 'idx_article_active', columns: ['active'])]
|
| 16 |
+
#[ORM\HasLifecycleCallbacks]
|
| 17 |
class Article
|
| 18 |
{
|
| 19 |
+
use UpdateTimestampTrait;
|
| 20 |
+
|
| 21 |
#[Groups(['article:read'])]
|
| 22 |
#[ORM\Id]
|
| 23 |
#[ORM\GeneratedValue(strategy: "IDENTITY")]
|
|
|
|
| 60 |
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
| 61 |
private ?string $content = null;
|
| 62 |
|
| 63 |
+
#[Groups(['article:read'])]
|
| 64 |
#[ORM\Column(name: 'update_at', type: Types::DATETIME_MUTABLE, nullable: true)]
|
| 65 |
private ?\DateTimeInterface $updateAt = null;
|
| 66 |
|
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
declare(strict_types=1);
|
| 4 |
+
|
| 5 |
+
namespace App\Entity\Behavior;
|
| 6 |
+
|
| 7 |
+
use Doctrine\ORM\Mapping as ORM;
|
| 8 |
+
|
| 9 |
+
/**
|
| 10 |
+
* Требует у класса-сущности свойство `$updateAt` (mapped column).
|
| 11 |
+
*
|
| 12 |
+
* @property \DateTimeInterface|null $updateAt
|
| 13 |
+
*/
|
| 14 |
+
trait UpdateTimestampTrait
|
| 15 |
+
{
|
| 16 |
+
#[ORM\PrePersist]
|
| 17 |
+
public function setInitialUpdateAt(): void
|
| 18 |
+
{
|
| 19 |
+
if ($this->updateAt === null) {
|
| 20 |
+
$this->updateAt = new \DateTimeImmutable();
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
#[ORM\PreUpdate]
|
| 25 |
+
public function refreshUpdateAt(): void
|
| 26 |
+
{
|
| 27 |
+
$this->updateAt = new \DateTimeImmutable();
|
| 28 |
+
}
|
| 29 |
+
}
|
|
@@ -2,6 +2,7 @@
|
|
| 2 |
|
| 3 |
namespace App\Entity;
|
| 4 |
|
|
|
|
| 5 |
use App\Repository\DiseaseRepository;
|
| 6 |
use Doctrine\DBAL\Types\Types;
|
| 7 |
use Doctrine\ORM\Mapping as ORM;
|
|
@@ -11,10 +12,14 @@ use Symfony\Component\Serializer\Annotation\Groups;
|
|
| 11 |
#[ORM\Table(name: 'disease')]
|
| 12 |
#[ORM\Index(name: 'idx_disease_region_id', columns: ['region_id'])]
|
| 13 |
#[ORM\Index(name: 'idx_disease_active', columns: ['active'])]
|
|
|
|
| 14 |
class Disease
|
| 15 |
{
|
|
|
|
|
|
|
| 16 |
#[Groups(['disease:read'])]
|
| 17 |
#[ORM\Id]
|
|
|
|
| 18 |
#[ORM\Column(type: Types::INTEGER)]
|
| 19 |
private ?int $id = null;
|
| 20 |
|
|
@@ -42,7 +47,7 @@ class Disease
|
|
| 42 |
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
| 43 |
private ?string $anons = null;
|
| 44 |
|
| 45 |
-
#[Groups(['disease:read'
|
| 46 |
#[ORM\Column(name: 'update_at', type: Types::DATETIME_MUTABLE, nullable: true)]
|
| 47 |
private ?\DateTimeInterface $updateAt = null;
|
| 48 |
|
|
|
|
| 2 |
|
| 3 |
namespace App\Entity;
|
| 4 |
|
| 5 |
+
use App\Entity\Behavior\UpdateTimestampTrait;
|
| 6 |
use App\Repository\DiseaseRepository;
|
| 7 |
use Doctrine\DBAL\Types\Types;
|
| 8 |
use Doctrine\ORM\Mapping as ORM;
|
|
|
|
| 12 |
#[ORM\Table(name: 'disease')]
|
| 13 |
#[ORM\Index(name: 'idx_disease_region_id', columns: ['region_id'])]
|
| 14 |
#[ORM\Index(name: 'idx_disease_active', columns: ['active'])]
|
| 15 |
+
#[ORM\HasLifecycleCallbacks]
|
| 16 |
class Disease
|
| 17 |
{
|
| 18 |
+
use UpdateTimestampTrait;
|
| 19 |
+
|
| 20 |
#[Groups(['disease:read'])]
|
| 21 |
#[ORM\Id]
|
| 22 |
+
#[ORM\GeneratedValue(strategy: "IDENTITY")]
|
| 23 |
#[ORM\Column(type: Types::INTEGER)]
|
| 24 |
private ?int $id = null;
|
| 25 |
|
|
|
|
| 47 |
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
| 48 |
private ?string $anons = null;
|
| 49 |
|
| 50 |
+
#[Groups(['disease:read'])]
|
| 51 |
#[ORM\Column(name: 'update_at', type: Types::DATETIME_MUTABLE, nullable: true)]
|
| 52 |
private ?\DateTimeInterface $updateAt = null;
|
| 53 |
|
|
@@ -2,6 +2,7 @@
|
|
| 2 |
|
| 3 |
namespace App\Entity;
|
| 4 |
|
|
|
|
| 5 |
use App\Repository\MedicalCenterRepository;
|
| 6 |
use Doctrine\DBAL\Types\Types;
|
| 7 |
use Doctrine\ORM\Mapping as ORM;
|
|
@@ -9,9 +10,13 @@ use Symfony\Component\Serializer\Annotation\Groups;
|
|
| 9 |
|
| 10 |
#[ORM\Entity(repositoryClass: MedicalCenterRepository::class)]
|
| 11 |
#[ORM\Table(name: 'medical_center')]
|
|
|
|
| 12 |
class MedicalCenter
|
| 13 |
{
|
|
|
|
|
|
|
| 14 |
#[ORM\Id]
|
|
|
|
| 15 |
#[ORM\Column(type: Types::INTEGER)]
|
| 16 |
#[Groups(['medical_center:read'])]
|
| 17 |
private ?int $id = null;
|
|
@@ -41,7 +46,7 @@ class MedicalCenter
|
|
| 41 |
private ?string $content = null;
|
| 42 |
|
| 43 |
#[ORM\Column(name: 'update_at', type: Types::DATETIME_MUTABLE, nullable: true)]
|
| 44 |
-
#[Groups(['medical_center:read'
|
| 45 |
private ?\DateTimeInterface $updateAt = null;
|
| 46 |
|
| 47 |
#[ORM\Column(name: 'kod_uslug', type: 'jsonb', nullable: true)]
|
|
|
|
| 2 |
|
| 3 |
namespace App\Entity;
|
| 4 |
|
| 5 |
+
use App\Entity\Behavior\UpdateTimestampTrait;
|
| 6 |
use App\Repository\MedicalCenterRepository;
|
| 7 |
use Doctrine\DBAL\Types\Types;
|
| 8 |
use Doctrine\ORM\Mapping as ORM;
|
|
|
|
| 10 |
|
| 11 |
#[ORM\Entity(repositoryClass: MedicalCenterRepository::class)]
|
| 12 |
#[ORM\Table(name: 'medical_center')]
|
| 13 |
+
#[ORM\HasLifecycleCallbacks]
|
| 14 |
class MedicalCenter
|
| 15 |
{
|
| 16 |
+
use UpdateTimestampTrait;
|
| 17 |
+
|
| 18 |
#[ORM\Id]
|
| 19 |
+
#[ORM\GeneratedValue(strategy: "IDENTITY")]
|
| 20 |
#[ORM\Column(type: Types::INTEGER)]
|
| 21 |
#[Groups(['medical_center:read'])]
|
| 22 |
private ?int $id = null;
|
|
|
|
| 46 |
private ?string $content = null;
|
| 47 |
|
| 48 |
#[ORM\Column(name: 'update_at', type: Types::DATETIME_MUTABLE, nullable: true)]
|
| 49 |
+
#[Groups(['medical_center:read'])]
|
| 50 |
private ?\DateTimeInterface $updateAt = null;
|
| 51 |
|
| 52 |
#[ORM\Column(name: 'kod_uslug', type: 'jsonb', nullable: true)]
|
|
@@ -2,6 +2,7 @@
|
|
| 2 |
|
| 3 |
namespace App\Entity;
|
| 4 |
|
|
|
|
| 5 |
use App\Repository\NewsRepository;
|
| 6 |
use Doctrine\DBAL\Types\Types;
|
| 7 |
use Doctrine\ORM\Mapping as ORM;
|
|
@@ -11,9 +12,13 @@ use Symfony\Component\Serializer\Annotation\Groups;
|
|
| 11 |
#[ORM\Table(name: 'news')]
|
| 12 |
#[ORM\Index(name: 'idx_news_region_id', columns: ['region_id'])]
|
| 13 |
#[ORM\Index(name: 'idx_news_active', columns: ['active'])]
|
|
|
|
| 14 |
class News
|
| 15 |
{
|
|
|
|
|
|
|
| 16 |
#[ORM\Id]
|
|
|
|
| 17 |
#[ORM\Column(type: Types::INTEGER)]
|
| 18 |
#[Groups(['news:read'])]
|
| 19 |
private ?int $id = null;
|
|
@@ -43,7 +48,7 @@ class News
|
|
| 43 |
private ?string $content = null;
|
| 44 |
|
| 45 |
#[ORM\Column(name: 'update_at', type: Types::DATETIME_MUTABLE, nullable: true)]
|
| 46 |
-
#[Groups(['news:read'
|
| 47 |
private ?\DateTimeInterface $updateAt = null;
|
| 48 |
|
| 49 |
#[ORM\Column(name: 'link_el_price', type: Types::TEXT, nullable: true)]
|
|
|
|
| 2 |
|
| 3 |
namespace App\Entity;
|
| 4 |
|
| 5 |
+
use App\Entity\Behavior\UpdateTimestampTrait;
|
| 6 |
use App\Repository\NewsRepository;
|
| 7 |
use Doctrine\DBAL\Types\Types;
|
| 8 |
use Doctrine\ORM\Mapping as ORM;
|
|
|
|
| 12 |
#[ORM\Table(name: 'news')]
|
| 13 |
#[ORM\Index(name: 'idx_news_region_id', columns: ['region_id'])]
|
| 14 |
#[ORM\Index(name: 'idx_news_active', columns: ['active'])]
|
| 15 |
+
#[ORM\HasLifecycleCallbacks]
|
| 16 |
class News
|
| 17 |
{
|
| 18 |
+
use UpdateTimestampTrait;
|
| 19 |
+
|
| 20 |
#[ORM\Id]
|
| 21 |
+
#[ORM\GeneratedValue(strategy: "IDENTITY")]
|
| 22 |
#[ORM\Column(type: Types::INTEGER)]
|
| 23 |
#[Groups(['news:read'])]
|
| 24 |
private ?int $id = null;
|
|
|
|
| 48 |
private ?string $content = null;
|
| 49 |
|
| 50 |
#[ORM\Column(name: 'update_at', type: Types::DATETIME_MUTABLE, nullable: true)]
|
| 51 |
+
#[Groups(['news:read'])]
|
| 52 |
private ?\DateTimeInterface $updateAt = null;
|
| 53 |
|
| 54 |
#[ORM\Column(name: 'link_el_price', type: Types::TEXT, nullable: true)]
|
|
@@ -2,6 +2,7 @@
|
|
| 2 |
|
| 3 |
namespace App\Entity;
|
| 4 |
|
|
|
|
| 5 |
use App\Repository\PromoRepository;
|
| 6 |
use Doctrine\DBAL\Types\Types;
|
| 7 |
use Doctrine\ORM\Mapping as ORM;
|
|
@@ -11,9 +12,13 @@ use Symfony\Component\Serializer\Annotation\Groups;
|
|
| 11 |
#[ORM\Table(name: 'promo')]
|
| 12 |
#[ORM\Index(name: 'idx_promo_region_id', columns: ['region_id'])]
|
| 13 |
#[ORM\Index(name: 'idx_promo_active', columns: ['active'])]
|
|
|
|
| 14 |
class Promo
|
| 15 |
{
|
|
|
|
|
|
|
| 16 |
#[ORM\Id]
|
|
|
|
| 17 |
#[ORM\Column(type: Types::INTEGER)]
|
| 18 |
#[Groups(['promo:read'])]
|
| 19 |
private ?int $id = null;
|
|
@@ -43,7 +48,7 @@ class Promo
|
|
| 43 |
private ?string $content = null;
|
| 44 |
|
| 45 |
#[ORM\Column(name: 'update_at', type: Types::DATETIME_MUTABLE, nullable: true)]
|
| 46 |
-
#[Groups(['promo:read'
|
| 47 |
private ?\DateTimeInterface $updateAt = null;
|
| 48 |
|
| 49 |
#[ORM\Column(type: 'jsonb', nullable: true)]
|
|
|
|
| 2 |
|
| 3 |
namespace App\Entity;
|
| 4 |
|
| 5 |
+
use App\Entity\Behavior\UpdateTimestampTrait;
|
| 6 |
use App\Repository\PromoRepository;
|
| 7 |
use Doctrine\DBAL\Types\Types;
|
| 8 |
use Doctrine\ORM\Mapping as ORM;
|
|
|
|
| 12 |
#[ORM\Table(name: 'promo')]
|
| 13 |
#[ORM\Index(name: 'idx_promo_region_id', columns: ['region_id'])]
|
| 14 |
#[ORM\Index(name: 'idx_promo_active', columns: ['active'])]
|
| 15 |
+
#[ORM\HasLifecycleCallbacks]
|
| 16 |
class Promo
|
| 17 |
{
|
| 18 |
+
use UpdateTimestampTrait;
|
| 19 |
+
|
| 20 |
#[ORM\Id]
|
| 21 |
+
#[ORM\GeneratedValue(strategy: "IDENTITY")]
|
| 22 |
#[ORM\Column(type: Types::INTEGER)]
|
| 23 |
#[Groups(['promo:read'])]
|
| 24 |
private ?int $id = null;
|
|
|
|
| 48 |
private ?string $content = null;
|
| 49 |
|
| 50 |
#[ORM\Column(name: 'update_at', type: Types::DATETIME_MUTABLE, nullable: true)]
|
| 51 |
+
#[Groups(['promo:read'])]
|
| 52 |
private ?\DateTimeInterface $updateAt = null;
|
| 53 |
|
| 54 |
#[ORM\Column(type: 'jsonb', nullable: true)]
|
|
@@ -2,6 +2,7 @@
|
|
| 2 |
|
| 3 |
namespace App\Entity;
|
| 4 |
|
|
|
|
| 5 |
use App\Repository\SiteServiceRepository;
|
| 6 |
use Doctrine\DBAL\Types\Types;
|
| 7 |
use Doctrine\ORM\Mapping as ORM;
|
|
@@ -11,9 +12,13 @@ use Symfony\Component\Serializer\Annotation\Groups;
|
|
| 11 |
#[ORM\Table(name: 'site_services')]
|
| 12 |
#[ORM\Index(name: 'idx_site_services_region_id', columns: ['region_id'])]
|
| 13 |
#[ORM\Index(name: 'idx_site_services_active', columns: ['active'])]
|
|
|
|
| 14 |
class SiteService
|
| 15 |
{
|
|
|
|
|
|
|
| 16 |
#[ORM\Id]
|
|
|
|
| 17 |
#[ORM\Column(type: Types::INTEGER)]
|
| 18 |
#[Groups(['site_service:read'])]
|
| 19 |
private ?int $id = null;
|
|
@@ -43,7 +48,7 @@ class SiteService
|
|
| 43 |
private ?string $content = null;
|
| 44 |
|
| 45 |
#[ORM\Column(name: 'update_at', type: Types::DATETIME_MUTABLE, nullable: true)]
|
| 46 |
-
#[Groups(['site_service:read'
|
| 47 |
private ?\DateTimeInterface $updateAt = null;
|
| 48 |
|
| 49 |
#[ORM\Column(name: 'link_videoreviews', type: 'jsonb', nullable: true)]
|
|
|
|
| 2 |
|
| 3 |
namespace App\Entity;
|
| 4 |
|
| 5 |
+
use App\Entity\Behavior\UpdateTimestampTrait;
|
| 6 |
use App\Repository\SiteServiceRepository;
|
| 7 |
use Doctrine\DBAL\Types\Types;
|
| 8 |
use Doctrine\ORM\Mapping as ORM;
|
|
|
|
| 12 |
#[ORM\Table(name: 'site_services')]
|
| 13 |
#[ORM\Index(name: 'idx_site_services_region_id', columns: ['region_id'])]
|
| 14 |
#[ORM\Index(name: 'idx_site_services_active', columns: ['active'])]
|
| 15 |
+
#[ORM\HasLifecycleCallbacks]
|
| 16 |
class SiteService
|
| 17 |
{
|
| 18 |
+
use UpdateTimestampTrait;
|
| 19 |
+
|
| 20 |
#[ORM\Id]
|
| 21 |
+
#[ORM\GeneratedValue(strategy: "IDENTITY")]
|
| 22 |
#[ORM\Column(type: Types::INTEGER)]
|
| 23 |
#[Groups(['site_service:read'])]
|
| 24 |
private ?int $id = null;
|
|
|
|
| 48 |
private ?string $content = null;
|
| 49 |
|
| 50 |
#[ORM\Column(name: 'update_at', type: Types::DATETIME_MUTABLE, nullable: true)]
|
| 51 |
+
#[Groups(['site_service:read'])]
|
| 52 |
private ?\DateTimeInterface $updateAt = null;
|
| 53 |
|
| 54 |
#[ORM\Column(name: 'link_videoreviews', type: 'jsonb', nullable: true)]
|
|
@@ -2,8 +2,10 @@
|
|
| 2 |
|
| 3 |
namespace App\Repository;
|
| 4 |
|
|
|
|
| 5 |
use App\Entity\Article;
|
| 6 |
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
|
|
|
| 7 |
use Doctrine\Persistence\ManagerRegistry;
|
| 8 |
|
| 9 |
/**
|
|
@@ -11,63 +13,34 @@ use Doctrine\Persistence\ManagerRegistry;
|
|
| 11 |
*/
|
| 12 |
class ArticleRepository extends ServiceEntityRepository
|
| 13 |
{
|
|
|
|
|
|
|
| 14 |
public function __construct(ManagerRegistry $registry)
|
| 15 |
{
|
| 16 |
parent::__construct($registry, Article::class);
|
| 17 |
}
|
| 18 |
|
| 19 |
-
|
|
|
|
|
|
|
| 20 |
{
|
| 21 |
-
$qb = $this->createQueryBuilder('a');
|
| 22 |
-
|
| 23 |
-
if (isset($filters['alias']) && $filters['alias'] !== '') {
|
| 24 |
-
$qb->andWhere('a.alias = :alias')
|
| 25 |
-
->setParameter('alias', $filters['alias']);
|
| 26 |
-
}
|
| 27 |
-
if (isset($filters['active']) && $filters['active'] !== '') {
|
| 28 |
-
$qb->andWhere('a.active = :active')
|
| 29 |
-
->setParameter('active', filter_var($filters['active'], FILTER_VALIDATE_BOOLEAN));
|
| 30 |
-
}
|
| 31 |
-
if (isset($filters['regionId']) && $filters['regionId'] !== '') {
|
| 32 |
-
$qb->andWhere('a.regionId = :regionId')
|
| 33 |
-
->setParameter('regionId', (int) $filters['regionId']);
|
| 34 |
-
}
|
| 35 |
|
| 36 |
-
$
|
| 37 |
|
| 38 |
-
|
| 39 |
-
->setMaxResults($limit);
|
| 40 |
-
|
| 41 |
-
return $qb->getQuery()->getResult();
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
public function countByFilters(array $filters): int
|
| 45 |
-
{
|
| 46 |
-
$qb = $this->createQueryBuilder('a')
|
| 47 |
-
->select('COUNT(a.id)');
|
| 48 |
-
|
| 49 |
-
if (isset($filters['alias']) && $filters['alias'] !== '') {
|
| 50 |
-
$qb->andWhere('a.alias = :alias')
|
| 51 |
-
->setParameter('alias', $filters['alias']);
|
| 52 |
-
}
|
| 53 |
-
if (isset($filters['active']) && $filters['active'] !== '') {
|
| 54 |
-
$qb->andWhere('a.active = :active')
|
| 55 |
-
->setParameter('active', filter_var($filters['active'], FILTER_VALIDATE_BOOLEAN));
|
| 56 |
-
}
|
| 57 |
-
if (isset($filters['regionId']) && $filters['regionId'] !== '') {
|
| 58 |
-
$qb->andWhere('a.regionId = :regionId')
|
| 59 |
-
->setParameter('regionId', (int) $filters['regionId']);
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
return (int) $qb->getQuery()->getSingleScalarResult();
|
| 63 |
}
|
| 64 |
|
|
|
|
|
|
|
|
|
|
| 65 |
public function findOneByAlias(string $alias): ?Article
|
| 66 |
{
|
| 67 |
$alias = trim($alias);
|
| 68 |
if ($alias === '') {
|
| 69 |
return null;
|
| 70 |
}
|
|
|
|
| 71 |
$variants = [
|
| 72 |
$alias,
|
| 73 |
$alias . '-',
|
|
@@ -79,16 +52,18 @@ class ArticleRepository extends ServiceEntityRepository
|
|
| 79 |
return $article;
|
| 80 |
}
|
| 81 |
}
|
| 82 |
-
|
|
|
|
| 83 |
$conn = $this->getEntityManager()->getConnection();
|
| 84 |
$id = $conn->fetchOne(
|
| 85 |
'SELECT id FROM article WHERE TRIM(alias) = :alias LIMIT 1',
|
| 86 |
['alias' => $alias],
|
| 87 |
-
['alias' => \PDO::PARAM_STR]
|
| 88 |
);
|
| 89 |
if ($id !== false) {
|
| 90 |
return $this->find($id);
|
| 91 |
}
|
|
|
|
| 92 |
return null;
|
| 93 |
}
|
| 94 |
}
|
|
|
|
| 2 |
|
| 3 |
namespace App\Repository;
|
| 4 |
|
| 5 |
+
use App\Dto\Content\ContentFilterDto;
|
| 6 |
use App\Entity\Article;
|
| 7 |
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
| 8 |
+
use Doctrine\ORM\QueryBuilder;
|
| 9 |
use Doctrine\Persistence\ManagerRegistry;
|
| 10 |
|
| 11 |
/**
|
|
|
|
| 13 |
*/
|
| 14 |
class ArticleRepository extends ServiceEntityRepository
|
| 15 |
{
|
| 16 |
+
use ContentFilterTrait;
|
| 17 |
+
|
| 18 |
public function __construct(ManagerRegistry $registry)
|
| 19 |
{
|
| 20 |
parent::__construct($registry, Article::class);
|
| 21 |
}
|
| 22 |
|
| 23 |
+
/**
|
| 24 |
+
*/
|
| 25 |
+
public function createFilteredQueryBuilder(ContentFilterDto $filters): QueryBuilder
|
| 26 |
{
|
| 27 |
+
$qb = $this->createQueryBuilder('a')->orderBy('a.id', 'DESC');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
+
$this->applyCommonFilters($qb, 'a', $filters);
|
| 30 |
|
| 31 |
+
return $qb;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
}
|
| 33 |
|
| 34 |
+
/**
|
| 35 |
+
* Поиск статьи по alias с учётом возможных вариантов написания (исторический функционал).
|
| 36 |
+
*/
|
| 37 |
public function findOneByAlias(string $alias): ?Article
|
| 38 |
{
|
| 39 |
$alias = trim($alias);
|
| 40 |
if ($alias === '') {
|
| 41 |
return null;
|
| 42 |
}
|
| 43 |
+
|
| 44 |
$variants = [
|
| 45 |
$alias,
|
| 46 |
$alias . '-',
|
|
|
|
| 52 |
return $article;
|
| 53 |
}
|
| 54 |
}
|
| 55 |
+
|
| 56 |
+
// Фолбэк по TRIM(alias) в БД для совместимости со старыми данными.
|
| 57 |
$conn = $this->getEntityManager()->getConnection();
|
| 58 |
$id = $conn->fetchOne(
|
| 59 |
'SELECT id FROM article WHERE TRIM(alias) = :alias LIMIT 1',
|
| 60 |
['alias' => $alias],
|
| 61 |
+
['alias' => \PDO::PARAM_STR],
|
| 62 |
);
|
| 63 |
if ($id !== false) {
|
| 64 |
return $this->find($id);
|
| 65 |
}
|
| 66 |
+
|
| 67 |
return null;
|
| 68 |
}
|
| 69 |
}
|
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
declare(strict_types=1);
|
| 4 |
+
|
| 5 |
+
namespace App\Repository;
|
| 6 |
+
|
| 7 |
+
use App\Dto\Content\ContentFilterDto;
|
| 8 |
+
use Doctrine\ORM\QueryBuilder;
|
| 9 |
+
|
| 10 |
+
/**
|
| 11 |
+
* Общие фильтры для контентных репозиториев (News/Promo/Disease/MedicalCenter/Article/SiteService).
|
| 12 |
+
*
|
| 13 |
+
* Trait подключается в Doctrine-репозитории, чтобы не держать бизнес-фильтры
|
| 14 |
+
* в статическом helper-классе и при этом не копировать одинаковые if-блоки.
|
| 15 |
+
*
|
| 16 |
+
* Поддерживается:
|
| 17 |
+
* - regionId / region_id: целое > 0;
|
| 18 |
+
* - active: bool;
|
| 19 |
+
* - alias: точное совпадение;
|
| 20 |
+
* - search / q: LIKE по lower-case значению заданного поля (по умолчанию `name`).
|
| 21 |
+
*
|
| 22 |
+
* Поле поиска параметризовано через $searchField на случай сущностей,
|
| 23 |
+
* где основное текстовое поле называется иначе (например, `title`).
|
| 24 |
+
* Если у сущности нет такого свойства, Doctrine упадёт с QueryException — это
|
| 25 |
+
* лучше ловится тестами на этапе разработки, чем 500 в проде.
|
| 26 |
+
*
|
| 27 |
+
* Важно: LOWER($alias.$searchField) при больших таблицах требует функционального
|
| 28 |
+
* индекса в PostgreSQL, например CREATE INDEX ... ON table (LOWER(name)).
|
| 29 |
+
*/
|
| 30 |
+
trait ContentFilterTrait
|
| 31 |
+
{
|
| 32 |
+
private function applyCommonFilters(
|
| 33 |
+
QueryBuilder $qb,
|
| 34 |
+
string $alias,
|
| 35 |
+
ContentFilterDto $filters,
|
| 36 |
+
string $searchField = 'name',
|
| 37 |
+
): void {
|
| 38 |
+
if ($filters->regionId !== null) {
|
| 39 |
+
$qb->andWhere("$alias.regionId = :regionId")
|
| 40 |
+
->setParameter('regionId', $filters->regionId);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
if ($filters->active !== null) {
|
| 44 |
+
$qb->andWhere("$alias.active = :active")
|
| 45 |
+
->setParameter('active', $filters->active);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
if ($filters->alias !== null) {
|
| 49 |
+
$qb->andWhere("$alias.alias = :aliasValue")
|
| 50 |
+
->setParameter('aliasValue', $filters->alias);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
if ($filters->search !== null) {
|
| 54 |
+
$qb->andWhere("LOWER($alias.$searchField) LIKE :search")
|
| 55 |
+
->setParameter('search', '%' . mb_strtolower($filters->search) . '%');
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
}
|
|
@@ -2,8 +2,10 @@
|
|
| 2 |
|
| 3 |
namespace App\Repository;
|
| 4 |
|
|
|
|
| 5 |
use App\Entity\Disease;
|
| 6 |
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
|
|
|
| 7 |
use Doctrine\Persistence\ManagerRegistry;
|
| 8 |
|
| 9 |
/**
|
|
@@ -14,8 +16,21 @@ use Doctrine\Persistence\ManagerRegistry;
|
|
| 14 |
*/
|
| 15 |
class DiseaseRepository extends ServiceEntityRepository
|
| 16 |
{
|
|
|
|
|
|
|
| 17 |
public function __construct(ManagerRegistry $registry)
|
| 18 |
{
|
| 19 |
parent::__construct($registry, Disease::class);
|
| 20 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
}
|
|
|
|
| 2 |
|
| 3 |
namespace App\Repository;
|
| 4 |
|
| 5 |
+
use App\Dto\Content\ContentFilterDto;
|
| 6 |
use App\Entity\Disease;
|
| 7 |
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
| 8 |
+
use Doctrine\ORM\QueryBuilder;
|
| 9 |
use Doctrine\Persistence\ManagerRegistry;
|
| 10 |
|
| 11 |
/**
|
|
|
|
| 16 |
*/
|
| 17 |
class DiseaseRepository extends ServiceEntityRepository
|
| 18 |
{
|
| 19 |
+
use ContentFilterTrait;
|
| 20 |
+
|
| 21 |
public function __construct(ManagerRegistry $registry)
|
| 22 |
{
|
| 23 |
parent::__construct($registry, Disease::class);
|
| 24 |
}
|
| 25 |
+
|
| 26 |
+
/**
|
| 27 |
+
*/
|
| 28 |
+
public function createFilteredQueryBuilder(ContentFilterDto $filters): QueryBuilder
|
| 29 |
+
{
|
| 30 |
+
$qb = $this->createQueryBuilder('d')->orderBy('d.id', 'ASC');
|
| 31 |
+
|
| 32 |
+
$this->applyCommonFilters($qb, 'd', $filters);
|
| 33 |
+
|
| 34 |
+
return $qb;
|
| 35 |
+
}
|
| 36 |
}
|
|
@@ -2,8 +2,10 @@
|
|
| 2 |
|
| 3 |
namespace App\Repository;
|
| 4 |
|
|
|
|
| 5 |
use App\Entity\MedicalCenter;
|
| 6 |
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
|
|
|
| 7 |
use Doctrine\Persistence\ManagerRegistry;
|
| 8 |
|
| 9 |
/**
|
|
@@ -14,8 +16,21 @@ use Doctrine\Persistence\ManagerRegistry;
|
|
| 14 |
*/
|
| 15 |
class MedicalCenterRepository extends ServiceEntityRepository
|
| 16 |
{
|
|
|
|
|
|
|
| 17 |
public function __construct(ManagerRegistry $registry)
|
| 18 |
{
|
| 19 |
parent::__construct($registry, MedicalCenter::class);
|
| 20 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
}
|
|
|
|
| 2 |
|
| 3 |
namespace App\Repository;
|
| 4 |
|
| 5 |
+
use App\Dto\Content\ContentFilterDto;
|
| 6 |
use App\Entity\MedicalCenter;
|
| 7 |
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
| 8 |
+
use Doctrine\ORM\QueryBuilder;
|
| 9 |
use Doctrine\Persistence\ManagerRegistry;
|
| 10 |
|
| 11 |
/**
|
|
|
|
| 16 |
*/
|
| 17 |
class MedicalCenterRepository extends ServiceEntityRepository
|
| 18 |
{
|
| 19 |
+
use ContentFilterTrait;
|
| 20 |
+
|
| 21 |
public function __construct(ManagerRegistry $registry)
|
| 22 |
{
|
| 23 |
parent::__construct($registry, MedicalCenter::class);
|
| 24 |
}
|
| 25 |
+
|
| 26 |
+
/**
|
| 27 |
+
*/
|
| 28 |
+
public function createFilteredQueryBuilder(ContentFilterDto $filters): QueryBuilder
|
| 29 |
+
{
|
| 30 |
+
$qb = $this->createQueryBuilder('m')->orderBy('m.id', 'DESC');
|
| 31 |
+
|
| 32 |
+
$this->applyCommonFilters($qb, 'm', $filters);
|
| 33 |
+
|
| 34 |
+
return $qb;
|
| 35 |
+
}
|
| 36 |
}
|
|
@@ -2,8 +2,10 @@
|
|
| 2 |
|
| 3 |
namespace App\Repository;
|
| 4 |
|
|
|
|
| 5 |
use App\Entity\News;
|
| 6 |
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
|
|
|
| 7 |
use Doctrine\Persistence\ManagerRegistry;
|
| 8 |
|
| 9 |
/**
|
|
@@ -14,8 +16,25 @@ use Doctrine\Persistence\ManagerRegistry;
|
|
| 14 |
*/
|
| 15 |
class NewsRepository extends ServiceEntityRepository
|
| 16 |
{
|
|
|
|
|
|
|
| 17 |
public function __construct(ManagerRegistry $registry)
|
| 18 |
{
|
| 19 |
parent::__construct($registry, News::class);
|
| 20 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
}
|
|
|
|
| 2 |
|
| 3 |
namespace App\Repository;
|
| 4 |
|
| 5 |
+
use App\Dto\Content\ContentFilterDto;
|
| 6 |
use App\Entity\News;
|
| 7 |
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
| 8 |
+
use Doctrine\ORM\QueryBuilder;
|
| 9 |
use Doctrine\Persistence\ManagerRegistry;
|
| 10 |
|
| 11 |
/**
|
|
|
|
| 16 |
*/
|
| 17 |
class NewsRepository extends ServiceEntityRepository
|
| 18 |
{
|
| 19 |
+
use ContentFilterTrait;
|
| 20 |
+
|
| 21 |
public function __construct(ManagerRegistry $registry)
|
| 22 |
{
|
| 23 |
parent::__construct($registry, News::class);
|
| 24 |
}
|
| 25 |
+
|
| 26 |
+
/**
|
| 27 |
+
* Готовит QueryBuilder под пагинацию (Pagerfanta\QueryAdapter).
|
| 28 |
+
*
|
| 29 |
+
* Поддерживаемые фильтры: regionId, active (по умолчанию true), alias, search.
|
| 30 |
+
*
|
| 31 |
+
*/
|
| 32 |
+
public function createFilteredQueryBuilder(ContentFilterDto $filters): QueryBuilder
|
| 33 |
+
{
|
| 34 |
+
$qb = $this->createQueryBuilder('n')->orderBy('n.id', 'DESC');
|
| 35 |
+
|
| 36 |
+
$this->applyCommonFilters($qb, 'n', $filters);
|
| 37 |
+
|
| 38 |
+
return $qb;
|
| 39 |
+
}
|
| 40 |
}
|
|
@@ -2,8 +2,10 @@
|
|
| 2 |
|
| 3 |
namespace App\Repository;
|
| 4 |
|
|
|
|
| 5 |
use App\Entity\Promo;
|
| 6 |
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
|
|
|
| 7 |
use Doctrine\Persistence\ManagerRegistry;
|
| 8 |
|
| 9 |
/**
|
|
@@ -14,8 +16,21 @@ use Doctrine\Persistence\ManagerRegistry;
|
|
| 14 |
*/
|
| 15 |
class PromoRepository extends ServiceEntityRepository
|
| 16 |
{
|
|
|
|
|
|
|
| 17 |
public function __construct(ManagerRegistry $registry)
|
| 18 |
{
|
| 19 |
parent::__construct($registry, Promo::class);
|
| 20 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
}
|
|
|
|
| 2 |
|
| 3 |
namespace App\Repository;
|
| 4 |
|
| 5 |
+
use App\Dto\Content\ContentFilterDto;
|
| 6 |
use App\Entity\Promo;
|
| 7 |
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
| 8 |
+
use Doctrine\ORM\QueryBuilder;
|
| 9 |
use Doctrine\Persistence\ManagerRegistry;
|
| 10 |
|
| 11 |
/**
|
|
|
|
| 16 |
*/
|
| 17 |
class PromoRepository extends ServiceEntityRepository
|
| 18 |
{
|
| 19 |
+
use ContentFilterTrait;
|
| 20 |
+
|
| 21 |
public function __construct(ManagerRegistry $registry)
|
| 22 |
{
|
| 23 |
parent::__construct($registry, Promo::class);
|
| 24 |
}
|
| 25 |
+
|
| 26 |
+
/**
|
| 27 |
+
*/
|
| 28 |
+
public function createFilteredQueryBuilder(ContentFilterDto $filters): QueryBuilder
|
| 29 |
+
{
|
| 30 |
+
$qb = $this->createQueryBuilder('p')->orderBy('p.id', 'DESC');
|
| 31 |
+
|
| 32 |
+
$this->applyCommonFilters($qb, 'p', $filters);
|
| 33 |
+
|
| 34 |
+
return $qb;
|
| 35 |
+
}
|
| 36 |
}
|
|
@@ -2,8 +2,10 @@
|
|
| 2 |
|
| 3 |
namespace App\Repository;
|
| 4 |
|
|
|
|
| 5 |
use App\Entity\SiteService;
|
| 6 |
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
|
|
|
| 7 |
use Doctrine\Persistence\ManagerRegistry;
|
| 8 |
|
| 9 |
/**
|
|
@@ -14,8 +16,21 @@ use Doctrine\Persistence\ManagerRegistry;
|
|
| 14 |
*/
|
| 15 |
class SiteServiceRepository extends ServiceEntityRepository
|
| 16 |
{
|
|
|
|
|
|
|
| 17 |
public function __construct(ManagerRegistry $registry)
|
| 18 |
{
|
| 19 |
parent::__construct($registry, SiteService::class);
|
| 20 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
}
|
|
|
|
| 2 |
|
| 3 |
namespace App\Repository;
|
| 4 |
|
| 5 |
+
use App\Dto\Content\ContentFilterDto;
|
| 6 |
use App\Entity\SiteService;
|
| 7 |
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
| 8 |
+
use Doctrine\ORM\QueryBuilder;
|
| 9 |
use Doctrine\Persistence\ManagerRegistry;
|
| 10 |
|
| 11 |
/**
|
|
|
|
| 16 |
*/
|
| 17 |
class SiteServiceRepository extends ServiceEntityRepository
|
| 18 |
{
|
| 19 |
+
use ContentFilterTrait;
|
| 20 |
+
|
| 21 |
public function __construct(ManagerRegistry $registry)
|
| 22 |
{
|
| 23 |
parent::__construct($registry, SiteService::class);
|
| 24 |
}
|
| 25 |
+
|
| 26 |
+
/**
|
| 27 |
+
*/
|
| 28 |
+
public function createFilteredQueryBuilder(ContentFilterDto $filters): QueryBuilder
|
| 29 |
+
{
|
| 30 |
+
$qb = $this->createQueryBuilder('s')->orderBy('s.id', 'ASC');
|
| 31 |
+
|
| 32 |
+
$this->applyCommonFilters($qb, 's', $filters);
|
| 33 |
+
|
| 34 |
+
return $qb;
|
| 35 |
+
}
|
| 36 |
}
|
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
declare(strict_types=1);
|
| 4 |
+
|
| 5 |
+
namespace App\Service\Crud;
|
| 6 |
+
|
| 7 |
+
use Doctrine\DBAL\Exception as DbalException;
|
| 8 |
+
use Doctrine\ORM\EntityManagerInterface;
|
| 9 |
+
use JsonException;
|
| 10 |
+
use Symfony\Component\HttpFoundation\JsonResponse;
|
| 11 |
+
use Symfony\Component\HttpFoundation\Request;
|
| 12 |
+
use Symfony\Component\HttpFoundation\Response;
|
| 13 |
+
use Symfony\Component\Serializer\Exception\ExceptionInterface as SerializerExceptionInterface;
|
| 14 |
+
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
| 15 |
+
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
| 16 |
+
use Symfony\Component\Serializer\SerializerInterface;
|
| 17 |
+
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
| 18 |
+
|
| 19 |
+
/**
|
| 20 |
+
* Универсальный CRUD-ответчик для тонких контент-контроллеров.
|
| 21 |
+
*
|
| 22 |
+
* Контракт ответов специально сохранён близким к старым *CrudService/контроллерам,
|
| 23 |
+
* чтобы не ломать существующих клиентов (фронтенд/мобильное):
|
| 24 |
+
* - валидация: HTTP 400 + сериализованный ConstraintViolationList
|
| 25 |
+
* (формат Symfony Serializer по умолчанию, т.е. RFC 7807 с ключом violations);
|
| 26 |
+
* - удаление с ошибкой БД (например, FK constraint): HTTP 500 + {error, message};
|
| 27 |
+
* - JSON-ключи запросов/ответов используют camelCase (см. свойства сущностей и группы *:write).
|
| 28 |
+
* Name converter в config/packages/serializer.yaml не задан намеренно — клиенту
|
| 29 |
+
* нужен консистентный camelCase, иначе незнакомые ключи будут проигнорированы.
|
| 30 |
+
*/
|
| 31 |
+
final class CrudResponder
|
| 32 |
+
{
|
| 33 |
+
public function __construct(
|
| 34 |
+
private EntityManagerInterface $em,
|
| 35 |
+
private SerializerInterface $serializer,
|
| 36 |
+
private DenormalizerInterface $denormalizer,
|
| 37 |
+
private ValidatorInterface $validator,
|
| 38 |
+
) {
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
/**
|
| 42 |
+
* @param list<string> $readGroups
|
| 43 |
+
*/
|
| 44 |
+
public function read(object $entity, array $readGroups): JsonResponse
|
| 45 |
+
{
|
| 46 |
+
return $this->json($entity, Response::HTTP_OK, $readGroups);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
/**
|
| 50 |
+
* @template T of object
|
| 51 |
+
*
|
| 52 |
+
* @param class-string<T> $entityClass
|
| 53 |
+
* @param list<string> $writeGroups
|
| 54 |
+
* @param list<string> $readGroups
|
| 55 |
+
*/
|
| 56 |
+
public function create(
|
| 57 |
+
Request $request,
|
| 58 |
+
string $entityClass,
|
| 59 |
+
array $writeGroups,
|
| 60 |
+
array $readGroups,
|
| 61 |
+
): JsonResponse {
|
| 62 |
+
$payload = $this->decodePayload($request);
|
| 63 |
+
if ($payload === null) {
|
| 64 |
+
return $this->jsonError('Ожидается JSON-объект в теле запроса', Response::HTTP_BAD_REQUEST);
|
| 65 |
+
}
|
| 66 |
+
unset($payload['id']);
|
| 67 |
+
|
| 68 |
+
try {
|
| 69 |
+
/** @var T $entity */
|
| 70 |
+
$entity = $this->denormalizer->denormalize(
|
| 71 |
+
$payload,
|
| 72 |
+
$entityClass,
|
| 73 |
+
null,
|
| 74 |
+
[
|
| 75 |
+
AbstractNormalizer::GROUPS => $writeGroups,
|
| 76 |
+
],
|
| 77 |
+
);
|
| 78 |
+
} catch (SerializerExceptionInterface $e) {
|
| 79 |
+
return $this->jsonError('Ошибка десериализации: ' . $e->getMessage(), Response::HTTP_BAD_REQUEST);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
if (($validationResponse = $this->validate($entity)) !== null) {
|
| 83 |
+
return $validationResponse;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
$this->em->persist($entity);
|
| 87 |
+
$this->em->flush();
|
| 88 |
+
|
| 89 |
+
return $this->json($entity, Response::HTTP_CREATED, $readGroups);
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
/**
|
| 93 |
+
* @param list<string> $writeGroups
|
| 94 |
+
* @param list<string> $readGroups
|
| 95 |
+
*/
|
| 96 |
+
public function update(
|
| 97 |
+
Request $request,
|
| 98 |
+
object $entity,
|
| 99 |
+
array $writeGroups,
|
| 100 |
+
array $readGroups,
|
| 101 |
+
): JsonResponse {
|
| 102 |
+
$payload = $this->decodePayload($request);
|
| 103 |
+
if ($payload === null) {
|
| 104 |
+
return $this->jsonError('Ожидается JSON-объект в теле запроса', Response::HTTP_BAD_REQUEST);
|
| 105 |
+
}
|
| 106 |
+
unset($payload['id']);
|
| 107 |
+
|
| 108 |
+
try {
|
| 109 |
+
$this->denormalizer->denormalize(
|
| 110 |
+
$payload,
|
| 111 |
+
$entity::class,
|
| 112 |
+
null,
|
| 113 |
+
[
|
| 114 |
+
AbstractNormalizer::GROUPS => $writeGroups,
|
| 115 |
+
AbstractNormalizer::OBJECT_TO_POPULATE => $entity,
|
| 116 |
+
],
|
| 117 |
+
);
|
| 118 |
+
} catch (SerializerExceptionInterface $e) {
|
| 119 |
+
return $this->jsonError('Ошибка десериализации: ' . $e->getMessage(), Response::HTTP_BAD_REQUEST);
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
if (($validationResponse = $this->validate($entity)) !== null) {
|
| 123 |
+
return $validationResponse;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
$this->em->flush();
|
| 127 |
+
|
| 128 |
+
return $this->json($entity, Response::HTTP_OK, $readGroups);
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
public function delete(object $entity): JsonResponse
|
| 132 |
+
{
|
| 133 |
+
try {
|
| 134 |
+
$this->em->remove($entity);
|
| 135 |
+
$this->em->flush();
|
| 136 |
+
} catch (DbalException $e) {
|
| 137 |
+
// Сохраняем легаси-контракт: при FK / NOT NULL / unique ошибках БД
|
| 138 |
+
// отдаём 500 + {error, message}. См. старый ArticleController::delete.
|
| 139 |
+
return new JsonResponse(
|
| 140 |
+
['error' => 'Ошибка при удалении записи', 'message' => $e->getMessage()],
|
| 141 |
+
Response::HTTP_INTERNAL_SERVER_ERROR,
|
| 142 |
+
);
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
return new JsonResponse(null, Response::HTTP_NO_CONTENT);
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
/**
|
| 149 |
+
* @return array<string, mixed>|null null если тело не является JSON-объектом
|
| 150 |
+
*
|
| 151 |
+
* Ловим как нативный \JsonException, так и Symfony\...\HttpFoundation\Exception\JsonException
|
| 152 |
+
* (последний наследует UnexpectedValueException, а не \JsonException, и без
|
| 153 |
+
* широкого перехвата Symfony ErrorListener перехватит ошибку до нашего try/catch).
|
| 154 |
+
*/
|
| 155 |
+
private function decodePayload(Request $request): ?array
|
| 156 |
+
{
|
| 157 |
+
try {
|
| 158 |
+
return $request->toArray();
|
| 159 |
+
} catch (JsonException|\UnexpectedValueException) {
|
| 160 |
+
return null;
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
private function validate(object $entity): ?JsonResponse
|
| 165 |
+
{
|
| 166 |
+
$errors = $this->validator->validate($entity);
|
| 167 |
+
if (count($errors) === 0) {
|
| 168 |
+
return null;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
// BC: легаси-контроллеры возвращали именно сериализованный ConstraintViolationList
|
| 172 |
+
// с кодом 400. Этот же формат продолжаем отдавать здесь, чтобы фронтенду
|
| 173 |
+
// не пришлось переписывать парсинг ошибок.
|
| 174 |
+
$json = $this->serializer->serialize($errors, 'json');
|
| 175 |
+
|
| 176 |
+
return new JsonResponse($json, Response::HTTP_BAD_REQUEST, [], true);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
/**
|
| 180 |
+
* @param list<string> $groups
|
| 181 |
+
*/
|
| 182 |
+
private function json(mixed $data, int $status, array $groups): JsonResponse
|
| 183 |
+
{
|
| 184 |
+
$json = $this->serializer->serialize($data, 'json', [
|
| 185 |
+
AbstractNormalizer::GROUPS => $groups,
|
| 186 |
+
]);
|
| 187 |
+
|
| 188 |
+
return new JsonResponse($json, $status, [], true);
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
private function jsonError(string $message, int $status): JsonResponse
|
| 192 |
+
{
|
| 193 |
+
return new JsonResponse(['error' => $message], $status);
|
| 194 |
+
}
|
| 195 |
+
}
|
|
@@ -2,206 +2,26 @@
|
|
| 2 |
|
| 3 |
namespace App\Service;
|
| 4 |
|
| 5 |
-
use App\Entity\Disease;
|
| 6 |
-
use App\Repository\DiseaseRepository;
|
| 7 |
use Doctrine\ORM\EntityManagerInterface;
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
final class DiseaseCrudService
|
| 10 |
{
|
| 11 |
public function __construct(
|
| 12 |
private EntityManagerInterface $em,
|
| 13 |
-
private DiseaseRepository $diseaseRepository,
|
| 14 |
) {
|
| 15 |
}
|
| 16 |
|
| 17 |
-
/**
|
| 18 |
-
* @return array{data: Disease[], total: int, page: int, per_page: int}
|
| 19 |
-
*/
|
| 20 |
-
public function getPaginatedList(int $page, int $perPage, ?int $regionId = null): array
|
| 21 |
-
{
|
| 22 |
-
$page = max(1, $page);
|
| 23 |
-
$perPage = min(max(1, $perPage), 500);
|
| 24 |
-
|
| 25 |
-
$qb = $this->diseaseRepository->createQueryBuilder('d')
|
| 26 |
-
->orderBy('d.id', 'ASC');
|
| 27 |
-
|
| 28 |
-
if ($regionId !== null) {
|
| 29 |
-
$qb->andWhere('d.regionId = :regionId')
|
| 30 |
-
->setParameter('regionId', $regionId);
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
$countQb = $this->diseaseRepository->createQueryBuilder('d')
|
| 34 |
-
->select('COUNT(d.id)');
|
| 35 |
-
if ($regionId !== null) {
|
| 36 |
-
$countQb->andWhere('d.regionId = :regionId')
|
| 37 |
-
->setParameter('regionId', $regionId);
|
| 38 |
-
}
|
| 39 |
-
$total = (int) $countQb->getQuery()->getSingleScalarResult();
|
| 40 |
-
|
| 41 |
-
$qb->setFirstResult(($page - 1) * $perPage)
|
| 42 |
-
->setMaxResults($perPage);
|
| 43 |
-
|
| 44 |
-
$data = $qb->getQuery()->getResult();
|
| 45 |
-
|
| 46 |
-
return [
|
| 47 |
-
'data' => $data,
|
| 48 |
-
'total' => $total,
|
| 49 |
-
'page' => $page,
|
| 50 |
-
'per_page' => $perPage,
|
| 51 |
-
];
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
public function getShow(int $id): ?Disease
|
| 55 |
-
{
|
| 56 |
-
return $this->diseaseRepository->find($id);
|
| 57 |
-
}
|
| 58 |
-
|
| 59 |
-
public function create(array $data): Disease
|
| 60 |
-
{
|
| 61 |
-
if (!array_key_exists('id', $data) || $data['id'] === null || $data['id'] === '') {
|
| 62 |
-
throw new \InvalidArgumentException('Поле id обязательно.');
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
$disease = new Disease();
|
| 66 |
-
$this->updateEntity($disease, $data);
|
| 67 |
-
|
| 68 |
-
$this->em->persist($disease);
|
| 69 |
-
$this->em->flush();
|
| 70 |
-
|
| 71 |
-
return $disease;
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
public function update(Disease $disease, array $data): Disease
|
| 75 |
-
{
|
| 76 |
-
unset($data['id']);
|
| 77 |
-
$this->updateEntity($disease, $data);
|
| 78 |
-
|
| 79 |
-
$this->em->flush();
|
| 80 |
-
|
| 81 |
-
return $disease;
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
public function delete(Disease $disease): void
|
| 85 |
-
{
|
| 86 |
-
$this->em->remove($disease);
|
| 87 |
-
$this->em->flush();
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
private function updateEntity(Disease $disease, array $data): void
|
| 91 |
-
{
|
| 92 |
-
if (array_key_exists('id', $data) && $data['id'] !== null && $data['id'] !== '') {
|
| 93 |
-
$disease->setId((int) $data['id']);
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
-
if (array_key_exists('name', $data)) {
|
| 97 |
-
$disease->setName($data['name']);
|
| 98 |
-
}
|
| 99 |
-
|
| 100 |
-
if (array_key_exists('previewPicture', $data) || array_key_exists('preview_picture', $data)) {
|
| 101 |
-
$disease->setPreviewPicture($data['previewPicture'] ?? $data['preview_picture']);
|
| 102 |
-
}
|
| 103 |
-
|
| 104 |
-
if (array_key_exists('active', $data)) {
|
| 105 |
-
$disease->setActive($data['active']);
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
if (array_key_exists('regionId', $data) || array_key_exists('region_id', $data)) {
|
| 109 |
-
$v = $data['regionId'] ?? $data['region_id'];
|
| 110 |
-
$disease->setRegionId($v === null || $v === '' ? null : (int) $v);
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
if (array_key_exists('alias', $data)) {
|
| 114 |
-
$disease->setAlias($data['alias']);
|
| 115 |
-
}
|
| 116 |
-
|
| 117 |
-
if (array_key_exists('anons', $data)) {
|
| 118 |
-
$disease->setAnons($data['anons']);
|
| 119 |
-
}
|
| 120 |
-
|
| 121 |
-
if (array_key_exists('updateAt', $data) || array_key_exists('update_at', $data)) {
|
| 122 |
-
$raw = $data['updateAt'] ?? $data['update_at'];
|
| 123 |
-
if ($raw === null || $raw === '') {
|
| 124 |
-
$disease->setUpdateAt(null);
|
| 125 |
-
} elseif ($raw instanceof \DateTimeInterface) {
|
| 126 |
-
$disease->setUpdateAt($raw);
|
| 127 |
-
} elseif (is_string($raw)) {
|
| 128 |
-
$disease->setUpdateAt(new \DateTimeImmutable($raw));
|
| 129 |
-
}
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
-
if (array_key_exists('hidePicture', $data) || array_key_exists('hide_picture', $data)) {
|
| 133 |
-
$disease->setHidePicture($data['hidePicture'] ?? $data['hide_picture']);
|
| 134 |
-
}
|
| 135 |
-
|
| 136 |
-
if (array_key_exists('readTime', $data) || array_key_exists('read_time', $data)) {
|
| 137 |
-
$disease->setReadTime($data['readTime'] ?? $data['read_time']);
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
if (array_key_exists('diseasesName', $data) || array_key_exists('diseases_name', $data)) {
|
| 141 |
-
$disease->setDiseasesName($data['diseasesName'] ?? $data['diseases_name']);
|
| 142 |
-
}
|
| 143 |
-
|
| 144 |
-
if (array_key_exists('tagsImportant', $data) || array_key_exists('tags_important', $data)) {
|
| 145 |
-
$disease->setTagsImportant($data['tagsImportant'] ?? $data['tags_important']);
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
if (array_key_exists('tags', $data)) {
|
| 149 |
-
$disease->setTags($data['tags']);
|
| 150 |
-
}
|
| 151 |
-
|
| 152 |
-
if (array_key_exists('diseasesOtherName', $data) || array_key_exists('diseases_other_name', $data)) {
|
| 153 |
-
$disease->setDiseasesOtherName($data['diseasesOtherName'] ?? $data['diseases_other_name']);
|
| 154 |
-
}
|
| 155 |
-
|
| 156 |
-
if (array_key_exists('symptom', $data)) {
|
| 157 |
-
$disease->setSymptom($data['symptom']);
|
| 158 |
-
}
|
| 159 |
-
|
| 160 |
-
if (array_key_exists('staff', $data)) {
|
| 161 |
-
$disease->setStaff($data['staff']);
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
if (array_key_exists('linkServices', $data) || array_key_exists('link_services', $data)) {
|
| 165 |
-
$disease->setLinkServices($data['linkServices'] ?? $data['link_services']);
|
| 166 |
-
}
|
| 167 |
-
|
| 168 |
-
if (array_key_exists('staffList', $data) || array_key_exists('staff_list', $data)) {
|
| 169 |
-
$disease->setStaffList($data['staffList'] ?? $data['staff_list']);
|
| 170 |
-
}
|
| 171 |
-
|
| 172 |
-
if (array_key_exists('staffPost', $data) || array_key_exists('staff_post', $data)) {
|
| 173 |
-
$disease->setStaffPost($data['staffPost'] ?? $data['staff_post']);
|
| 174 |
-
}
|
| 175 |
-
|
| 176 |
-
if (array_key_exists('staffPostExclude', $data) || array_key_exists('staff_post_exclude', $data)) {
|
| 177 |
-
$disease->setStaffPostExclude($data['staffPostExclude'] ?? $data['staff_post_exclude']);
|
| 178 |
-
}
|
| 179 |
-
|
| 180 |
-
if (array_key_exists('linkFaq', $data) || array_key_exists('link_faq', $data)) {
|
| 181 |
-
$disease->setLinkFaq($data['linkFaq'] ?? $data['link_faq']);
|
| 182 |
-
}
|
| 183 |
-
|
| 184 |
-
if (array_key_exists('bibliography', $data)) {
|
| 185 |
-
$disease->setBibliography($data['bibliography']);
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
if (array_key_exists('staffCheck', $data) || array_key_exists('staff_check', $data)) {
|
| 189 |
-
$disease->setStaffCheck($data['staffCheck'] ?? $data['staff_check']);
|
| 190 |
-
}
|
| 191 |
-
|
| 192 |
-
if (array_key_exists('content', $data)) {
|
| 193 |
-
$disease->setContent($data['content']);
|
| 194 |
-
}
|
| 195 |
-
}
|
| 196 |
-
|
| 197 |
public function syncFromViewDisease(string $viewName = 'public.view_disease'): int
|
| 198 |
{
|
| 199 |
if (!preg_match('/^[A-Za-z0-9_\.]+$/', $viewName)) {
|
| 200 |
throw new \InvalidArgumentException('Invalid view name');
|
| 201 |
}
|
| 202 |
|
| 203 |
-
$connection = $this->em->getConnection();
|
| 204 |
-
|
| 205 |
$sql = sprintf(
|
| 206 |
'INSERT INTO disease (
|
| 207 |
id,
|
|
@@ -282,6 +102,6 @@ final class DiseaseCrudService
|
|
| 282 |
$viewName
|
| 283 |
);
|
| 284 |
|
| 285 |
-
return (int) $
|
| 286 |
}
|
| 287 |
}
|
|
|
|
| 2 |
|
| 3 |
namespace App\Service;
|
| 4 |
|
|
|
|
|
|
|
| 5 |
use Doctrine\ORM\EntityManagerInterface;
|
| 6 |
|
| 7 |
+
/**
|
| 8 |
+
* Импорт заболеваний из материализованного представления (Bitrix view).
|
| 9 |
+
*
|
| 10 |
+
* См. DiseaseController + CrudResponder для CRUD; этот сервис — только syncFromView*.
|
| 11 |
+
*/
|
| 12 |
final class DiseaseCrudService
|
| 13 |
{
|
| 14 |
public function __construct(
|
| 15 |
private EntityManagerInterface $em,
|
|
|
|
| 16 |
) {
|
| 17 |
}
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
public function syncFromViewDisease(string $viewName = 'public.view_disease'): int
|
| 20 |
{
|
| 21 |
if (!preg_match('/^[A-Za-z0-9_\.]+$/', $viewName)) {
|
| 22 |
throw new \InvalidArgumentException('Invalid view name');
|
| 23 |
}
|
| 24 |
|
|
|
|
|
|
|
| 25 |
$sql = sprintf(
|
| 26 |
'INSERT INTO disease (
|
| 27 |
id,
|
|
|
|
| 102 |
$viewName
|
| 103 |
);
|
| 104 |
|
| 105 |
+
return (int) $this->em->getConnection()->executeStatement($sql);
|
| 106 |
}
|
| 107 |
}
|
|
@@ -2,312 +2,127 @@
|
|
| 2 |
|
| 3 |
namespace App\Service;
|
| 4 |
|
| 5 |
-
use App\Entity\MedicalCenter;
|
| 6 |
-
use App\Repository\MedicalCenterRepository;
|
| 7 |
use Doctrine\ORM\EntityManagerInterface;
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
final class MedicalCenterCrudService
|
| 10 |
{
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
if (array_key_exists('mainLinkStaff', $data) || array_key_exists('main_link_staff', $data)) {
|
| 127 |
-
$medicalCenter->setMainLinkStaff($data['mainLinkStaff'] ?? $data['main_link_staff']);
|
| 128 |
-
}
|
| 129 |
-
|
| 130 |
-
if (array_key_exists('contraindications', $data)) {
|
| 131 |
-
$medicalCenter->setContraindications($data['contraindications']);
|
| 132 |
-
}
|
| 133 |
-
|
| 134 |
-
if (array_key_exists('hidePicture', $data) || array_key_exists('hide_picture', $data)) {
|
| 135 |
-
$v = $data['hidePicture'] ?? $data['hide_picture'];
|
| 136 |
-
$medicalCenter->setHidePicture($v === null || $v === '' ? null : (int) $v);
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
-
if (array_key_exists('indications', $data)) {
|
| 140 |
-
$medicalCenter->setIndications($data['indications']);
|
| 141 |
-
}
|
| 142 |
-
|
| 143 |
-
if (array_key_exists('linkSale', $data) || array_key_exists('link_sale', $data)) {
|
| 144 |
-
$medicalCenter->setLinkSale($data['linkSale'] ?? $data['link_sale']);
|
| 145 |
-
}
|
| 146 |
-
|
| 147 |
-
if (array_key_exists('plusList', $data) || array_key_exists('plus_list', $data)) {
|
| 148 |
-
$medicalCenter->setPlusList($data['plusList'] ?? $data['plus_list']);
|
| 149 |
-
}
|
| 150 |
-
|
| 151 |
-
if (array_key_exists('plusText', $data) || array_key_exists('plus_text', $data)) {
|
| 152 |
-
$medicalCenter->setPlusText($data['plusText'] ?? $data['plus_text']);
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
if (array_key_exists('plusTitle', $data) || array_key_exists('plus_title', $data)) {
|
| 156 |
-
$medicalCenter->setPlusTitle($data['plusTitle'] ?? $data['plus_title']);
|
| 157 |
-
}
|
| 158 |
-
|
| 159 |
-
if (array_key_exists('processText', $data) || array_key_exists('process_text', $data)) {
|
| 160 |
-
$medicalCenter->setProcessText($data['processText'] ?? $data['process_text']);
|
| 161 |
-
}
|
| 162 |
-
|
| 163 |
-
if (array_key_exists('processTitle', $data) || array_key_exists('process_title', $data)) {
|
| 164 |
-
$medicalCenter->setProcessTitle($data['processTitle'] ?? $data['process_title']);
|
| 165 |
-
}
|
| 166 |
-
|
| 167 |
-
if (array_key_exists('servicesList', $data) || array_key_exists('services_list', $data)) {
|
| 168 |
-
$medicalCenter->setServicesList($data['servicesList'] ?? $data['services_list']);
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
if (array_key_exists('servicesPhotos', $data) || array_key_exists('services_photos', $data)) {
|
| 172 |
-
$medicalCenter->setServicesPhotos($data['servicesPhotos'] ?? $data['services_photos']);
|
| 173 |
-
}
|
| 174 |
-
|
| 175 |
-
if (array_key_exists('servicesTitle', $data) || array_key_exists('services_title', $data)) {
|
| 176 |
-
$medicalCenter->setServicesTitle($data['servicesTitle'] ?? $data['services_title']);
|
| 177 |
-
}
|
| 178 |
-
|
| 179 |
-
if (array_key_exists('sortStaff', $data) || array_key_exists('sort_staff', $data)) {
|
| 180 |
-
$medicalCenter->setSortStaff($data['sortStaff'] ?? $data['sort_staff']);
|
| 181 |
-
}
|
| 182 |
-
|
| 183 |
-
if (array_key_exists('trainingText', $data) || array_key_exists('training_text', $data)) {
|
| 184 |
-
$medicalCenter->setTrainingText($data['trainingText'] ?? $data['training_text']);
|
| 185 |
-
}
|
| 186 |
-
|
| 187 |
-
if (array_key_exists('trainingTextTitle', $data) || array_key_exists('training_text_title', $data)) {
|
| 188 |
-
$medicalCenter->setTrainingTextTitle($data['trainingTextTitle'] ?? $data['training_text_title']);
|
| 189 |
-
}
|
| 190 |
-
|
| 191 |
-
if (array_key_exists('whyText', $data) || array_key_exists('why_text', $data)) {
|
| 192 |
-
$medicalCenter->setWhyText($data['whyText'] ?? $data['why_text']);
|
| 193 |
-
}
|
| 194 |
-
|
| 195 |
-
if (array_key_exists('whyTitle', $data) || array_key_exists('why_title', $data)) {
|
| 196 |
-
$medicalCenter->setWhyTitle($data['whyTitle'] ?? $data['why_title']);
|
| 197 |
-
}
|
| 198 |
-
}
|
| 199 |
-
|
| 200 |
-
public function syncFromViewCenters(string $viewName = 'public.view_centers'): int
|
| 201 |
-
{
|
| 202 |
-
// В опции разрешаем только идентификаторы (буквы/цифры/underscore) и точку для схемы.
|
| 203 |
-
if (!preg_match('/^[A-Za-z0-9_\.]+$/', $viewName)) {
|
| 204 |
-
throw new \InvalidArgumentException('Invalid view name');
|
| 205 |
-
}
|
| 206 |
-
|
| 207 |
-
$connection = $this->em->getConnection();
|
| 208 |
-
|
| 209 |
-
$sql = sprintf(
|
| 210 |
-
'INSERT INTO medical_center (
|
| 211 |
-
id,
|
| 212 |
-
name,
|
| 213 |
-
active,
|
| 214 |
-
region_id,
|
| 215 |
-
alias,
|
| 216 |
-
anons,
|
| 217 |
-
content,
|
| 218 |
-
update_at,
|
| 219 |
-
kod_uslug,
|
| 220 |
-
doctors,
|
| 221 |
-
services,
|
| 222 |
-
articles,
|
| 223 |
-
txt_up,
|
| 224 |
-
main_link_staff,
|
| 225 |
-
contraindications,
|
| 226 |
-
hide_picture,
|
| 227 |
-
indications,
|
| 228 |
-
link_sale,
|
| 229 |
-
plus_list,
|
| 230 |
-
plus_text,
|
| 231 |
-
plus_title,
|
| 232 |
-
process_text,
|
| 233 |
-
process_title,
|
| 234 |
-
services_list,
|
| 235 |
-
services_photos,
|
| 236 |
-
services_title,
|
| 237 |
-
sort_staff,
|
| 238 |
-
training_text,
|
| 239 |
-
training_text_title,
|
| 240 |
-
why_text,
|
| 241 |
-
why_title
|
| 242 |
-
)
|
| 243 |
-
SELECT
|
| 244 |
-
id,
|
| 245 |
-
name,
|
| 246 |
-
active,
|
| 247 |
-
region_id,
|
| 248 |
-
alias,
|
| 249 |
-
anons,
|
| 250 |
-
content,
|
| 251 |
-
update_at,
|
| 252 |
-
kod_uslug,
|
| 253 |
-
doctors,
|
| 254 |
-
services,
|
| 255 |
-
articles,
|
| 256 |
-
txt_up,
|
| 257 |
-
main_link_staff,
|
| 258 |
-
contraindications,
|
| 259 |
-
hide_picture,
|
| 260 |
-
indications,
|
| 261 |
-
link_sale,
|
| 262 |
-
plus_list,
|
| 263 |
-
plus_text,
|
| 264 |
-
plus_title,
|
| 265 |
-
process_text,
|
| 266 |
-
process_title,
|
| 267 |
-
services_list,
|
| 268 |
-
services_photos,
|
| 269 |
-
services_title,
|
| 270 |
-
sort_staff,
|
| 271 |
-
training_text,
|
| 272 |
-
training_text_title,
|
| 273 |
-
why_text,
|
| 274 |
-
why_title
|
| 275 |
-
FROM %s
|
| 276 |
-
ON CONFLICT (id) DO UPDATE SET
|
| 277 |
-
name = EXCLUDED.name,
|
| 278 |
-
active = EXCLUDED.active,
|
| 279 |
-
region_id = EXCLUDED.region_id,
|
| 280 |
-
alias = EXCLUDED.alias,
|
| 281 |
-
anons = EXCLUDED.anons,
|
| 282 |
-
content = EXCLUDED.content,
|
| 283 |
-
update_at = EXCLUDED.update_at,
|
| 284 |
-
kod_uslug = EXCLUDED.kod_uslug,
|
| 285 |
-
doctors = EXCLUDED.doctors,
|
| 286 |
-
services = EXCLUDED.services,
|
| 287 |
-
articles = EXCLUDED.articles,
|
| 288 |
-
txt_up = EXCLUDED.txt_up,
|
| 289 |
-
main_link_staff = EXCLUDED.main_link_staff,
|
| 290 |
-
contraindications = EXCLUDED.contraindications,
|
| 291 |
-
hide_picture = EXCLUDED.hide_picture,
|
| 292 |
-
indications = EXCLUDED.indications,
|
| 293 |
-
link_sale = EXCLUDED.link_sale,
|
| 294 |
-
plus_list = EXCLUDED.plus_list,
|
| 295 |
-
plus_text = EXCLUDED.plus_text,
|
| 296 |
-
plus_title = EXCLUDED.plus_title,
|
| 297 |
-
process_text = EXCLUDED.process_text,
|
| 298 |
-
process_title = EXCLUDED.process_title,
|
| 299 |
-
services_list = EXCLUDED.services_list,
|
| 300 |
-
services_photos = EXCLUDED.services_photos,
|
| 301 |
-
services_title = EXCLUDED.services_title,
|
| 302 |
-
sort_staff = EXCLUDED.sort_staff,
|
| 303 |
-
training_text = EXCLUDED.training_text,
|
| 304 |
-
training_text_title = EXCLUDED.training_text_title,
|
| 305 |
-
why_text = EXCLUDED.why_text,
|
| 306 |
-
why_title = EXCLUDED.why_title',
|
| 307 |
-
$viewName
|
| 308 |
-
);
|
| 309 |
-
|
| 310 |
-
return (int) $connection->executeStatement($sql);
|
| 311 |
-
}
|
| 312 |
}
|
| 313 |
-
|
|
|
|
| 2 |
|
| 3 |
namespace App\Service;
|
| 4 |
|
|
|
|
|
|
|
| 5 |
use Doctrine\ORM\EntityManagerInterface;
|
| 6 |
|
| 7 |
+
/**
|
| 8 |
+
* Импорт центров из материализованного представления (Bitrix view).
|
| 9 |
+
*
|
| 10 |
+
* См. MedicalCenterController + CrudResponder для CRUD; этот сервис — только syncFromView*.
|
| 11 |
+
*/
|
| 12 |
final class MedicalCenterCrudService
|
| 13 |
{
|
| 14 |
+
public function __construct(
|
| 15 |
+
private EntityManagerInterface $em,
|
| 16 |
+
) {
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
public function syncFromViewCenters(string $viewName = 'public.view_centers'): int
|
| 20 |
+
{
|
| 21 |
+
if (!preg_match('/^[A-Za-z0-9_\.]+$/', $viewName)) {
|
| 22 |
+
throw new \InvalidArgumentException('Invalid view name');
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
$sql = sprintf(
|
| 26 |
+
'INSERT INTO medical_center (
|
| 27 |
+
id,
|
| 28 |
+
name,
|
| 29 |
+
active,
|
| 30 |
+
region_id,
|
| 31 |
+
alias,
|
| 32 |
+
anons,
|
| 33 |
+
content,
|
| 34 |
+
update_at,
|
| 35 |
+
kod_uslug,
|
| 36 |
+
doctors,
|
| 37 |
+
services,
|
| 38 |
+
articles,
|
| 39 |
+
txt_up,
|
| 40 |
+
main_link_staff,
|
| 41 |
+
contraindications,
|
| 42 |
+
hide_picture,
|
| 43 |
+
indications,
|
| 44 |
+
link_sale,
|
| 45 |
+
plus_list,
|
| 46 |
+
plus_text,
|
| 47 |
+
plus_title,
|
| 48 |
+
process_text,
|
| 49 |
+
process_title,
|
| 50 |
+
services_list,
|
| 51 |
+
services_photos,
|
| 52 |
+
services_title,
|
| 53 |
+
sort_staff,
|
| 54 |
+
training_text,
|
| 55 |
+
training_text_title,
|
| 56 |
+
why_text,
|
| 57 |
+
why_title
|
| 58 |
+
)
|
| 59 |
+
SELECT
|
| 60 |
+
id,
|
| 61 |
+
name,
|
| 62 |
+
active,
|
| 63 |
+
region_id,
|
| 64 |
+
alias,
|
| 65 |
+
anons,
|
| 66 |
+
content,
|
| 67 |
+
update_at,
|
| 68 |
+
kod_uslug,
|
| 69 |
+
doctors,
|
| 70 |
+
services,
|
| 71 |
+
articles,
|
| 72 |
+
txt_up,
|
| 73 |
+
main_link_staff,
|
| 74 |
+
contraindications,
|
| 75 |
+
hide_picture,
|
| 76 |
+
indications,
|
| 77 |
+
link_sale,
|
| 78 |
+
plus_list,
|
| 79 |
+
plus_text,
|
| 80 |
+
plus_title,
|
| 81 |
+
process_text,
|
| 82 |
+
process_title,
|
| 83 |
+
services_list,
|
| 84 |
+
services_photos,
|
| 85 |
+
services_title,
|
| 86 |
+
sort_staff,
|
| 87 |
+
training_text,
|
| 88 |
+
training_text_title,
|
| 89 |
+
why_text,
|
| 90 |
+
why_title
|
| 91 |
+
FROM %s
|
| 92 |
+
ON CONFLICT (id) DO UPDATE SET
|
| 93 |
+
name = EXCLUDED.name,
|
| 94 |
+
active = EXCLUDED.active,
|
| 95 |
+
region_id = EXCLUDED.region_id,
|
| 96 |
+
alias = EXCLUDED.alias,
|
| 97 |
+
anons = EXCLUDED.anons,
|
| 98 |
+
content = EXCLUDED.content,
|
| 99 |
+
update_at = EXCLUDED.update_at,
|
| 100 |
+
kod_uslug = EXCLUDED.kod_uslug,
|
| 101 |
+
doctors = EXCLUDED.doctors,
|
| 102 |
+
services = EXCLUDED.services,
|
| 103 |
+
articles = EXCLUDED.articles,
|
| 104 |
+
txt_up = EXCLUDED.txt_up,
|
| 105 |
+
main_link_staff = EXCLUDED.main_link_staff,
|
| 106 |
+
contraindications = EXCLUDED.contraindications,
|
| 107 |
+
hide_picture = EXCLUDED.hide_picture,
|
| 108 |
+
indications = EXCLUDED.indications,
|
| 109 |
+
link_sale = EXCLUDED.link_sale,
|
| 110 |
+
plus_list = EXCLUDED.plus_list,
|
| 111 |
+
plus_text = EXCLUDED.plus_text,
|
| 112 |
+
plus_title = EXCLUDED.plus_title,
|
| 113 |
+
process_text = EXCLUDED.process_text,
|
| 114 |
+
process_title = EXCLUDED.process_title,
|
| 115 |
+
services_list = EXCLUDED.services_list,
|
| 116 |
+
services_photos = EXCLUDED.services_photos,
|
| 117 |
+
services_title = EXCLUDED.services_title,
|
| 118 |
+
sort_staff = EXCLUDED.sort_staff,
|
| 119 |
+
training_text = EXCLUDED.training_text,
|
| 120 |
+
training_text_title = EXCLUDED.training_text_title,
|
| 121 |
+
why_text = EXCLUDED.why_text,
|
| 122 |
+
why_title = EXCLUDED.why_title',
|
| 123 |
+
$viewName
|
| 124 |
+
);
|
| 125 |
+
|
| 126 |
+
return (int) $this->em->getConnection()->executeStatement($sql);
|
| 127 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
}
|
|
|
|
@@ -2,148 +2,28 @@
|
|
| 2 |
|
| 3 |
namespace App\Service;
|
| 4 |
|
| 5 |
-
use App\Entity\News;
|
| 6 |
-
use App\Repository\NewsRepository;
|
| 7 |
use Doctrine\ORM\EntityManagerInterface;
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
final class NewsCrudService
|
| 10 |
{
|
| 11 |
public function __construct(
|
| 12 |
private EntityManagerInterface $em,
|
| 13 |
-
private NewsRepository $newsRepository
|
| 14 |
) {
|
| 15 |
}
|
| 16 |
|
| 17 |
-
/**
|
| 18 |
-
* @return News[]
|
| 19 |
-
*/
|
| 20 |
-
public function getList(?int $regionId = null, ?bool $active = true): array
|
| 21 |
-
{
|
| 22 |
-
$criteria = [];
|
| 23 |
-
if ($regionId !== null) {
|
| 24 |
-
$criteria['regionId'] = $regionId;
|
| 25 |
-
}
|
| 26 |
-
if ($active !== null) {
|
| 27 |
-
$criteria['active'] = $active;
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
return $this->newsRepository->findBy($criteria, ['id' => 'ASC']);
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
public function getShow(int $id): ?News
|
| 34 |
-
{
|
| 35 |
-
return $this->newsRepository->find($id);
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
public function create(array $data): News
|
| 39 |
-
{
|
| 40 |
-
$news = new News();
|
| 41 |
-
$this->updateEntity($news, $data);
|
| 42 |
-
|
| 43 |
-
$this->em->persist($news);
|
| 44 |
-
$this->em->flush();
|
| 45 |
-
|
| 46 |
-
return $news;
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
public function update(News $news, array $data): News
|
| 50 |
-
{
|
| 51 |
-
unset($data['id']);
|
| 52 |
-
$this->updateEntity($news, $data);
|
| 53 |
-
|
| 54 |
-
$this->em->flush();
|
| 55 |
-
return $news;
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
public function delete(News $news): void
|
| 59 |
-
{
|
| 60 |
-
$this->em->remove($news);
|
| 61 |
-
$this->em->flush();
|
| 62 |
-
}
|
| 63 |
-
|
| 64 |
-
private function updateEntity(News $news, array $data): void
|
| 65 |
-
{
|
| 66 |
-
if (array_key_exists('id', $data) && $data['id'] !== null && $data['id'] !== '') {
|
| 67 |
-
$news->setId((int) $data['id']);
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
if (array_key_exists('name', $data)) {
|
| 71 |
-
$news->setName($data['name']);
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
if (array_key_exists('active', $data)) {
|
| 75 |
-
$news->setActive($data['active']);
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
if (array_key_exists('regionId', $data) || array_key_exists('region_id', $data)) {
|
| 79 |
-
$v = $data['regionId'] ?? $data['region_id'];
|
| 80 |
-
$news->setRegionId($v === null || $v === '' ? null : (int) $v);
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
if (array_key_exists('alias', $data)) {
|
| 84 |
-
$news->setAlias($data['alias']);
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
if (array_key_exists('anons', $data)) {
|
| 88 |
-
$news->setAnons($data['anons']);
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
if (array_key_exists('content', $data)) {
|
| 92 |
-
$news->setContent($data['content']);
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
if (array_key_exists('updateAt', $data) || array_key_exists('update_at', $data)) {
|
| 96 |
-
$raw = $data['updateAt'] ?? $data['update_at'];
|
| 97 |
-
if ($raw === null || $raw === '') {
|
| 98 |
-
$news->setUpdateAt(null);
|
| 99 |
-
} elseif ($raw instanceof \DateTimeInterface) {
|
| 100 |
-
$news->setUpdateAt($raw);
|
| 101 |
-
} elseif (is_string($raw)) {
|
| 102 |
-
$news->setUpdateAt(new \DateTimeImmutable($raw));
|
| 103 |
-
}
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
-
if (array_key_exists('linkElPrice', $data) || array_key_exists('link_el_price', $data)) {
|
| 107 |
-
$news->setLinkElPrice($data['linkElPrice'] ?? $data['link_el_price']);
|
| 108 |
-
}
|
| 109 |
-
|
| 110 |
-
if (array_key_exists('shortName', $data) || array_key_exists('short_name', $data)) {
|
| 111 |
-
$news->setShortName($data['shortName'] ?? $data['short_name']);
|
| 112 |
-
}
|
| 113 |
-
|
| 114 |
-
if (array_key_exists('timer', $data)) {
|
| 115 |
-
$news->setTimer($data['timer']);
|
| 116 |
-
}
|
| 117 |
-
|
| 118 |
-
if (array_key_exists('timerBg', $data) || array_key_exists('timer_bg', $data)) {
|
| 119 |
-
$news->setTimerBg($data['timerBg'] ?? $data['timer_bg']);
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
if (array_key_exists('formOrder', $data) || array_key_exists('form_order', $data)) {
|
| 123 |
-
$news->setFormOrder($data['formOrder'] ?? $data['form_order']);
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
if (array_key_exists('linkServices', $data) || array_key_exists('link_services', $data)) {
|
| 127 |
-
$news->setLinkServices($data['linkServices'] ?? $data['link_services']);
|
| 128 |
-
}
|
| 129 |
-
|
| 130 |
-
if (array_key_exists('linkStaff', $data) || array_key_exists('link_staff', $data)) {
|
| 131 |
-
$news->setLinkStaff($data['linkStaff'] ?? $data['link_staff']);
|
| 132 |
-
}
|
| 133 |
-
|
| 134 |
-
if (array_key_exists('photos', $data)) {
|
| 135 |
-
$news->setPhotos($data['photos']);
|
| 136 |
-
}
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
public function syncFromViewNews(string $viewName = 'public.view_news'): int
|
| 140 |
{
|
| 141 |
if (!preg_match('/^[A-Za-z0-9_\.]+$/', $viewName)) {
|
| 142 |
throw new \InvalidArgumentException('Invalid view name');
|
| 143 |
}
|
| 144 |
|
| 145 |
-
$connection = $this->em->getConnection();
|
| 146 |
-
|
| 147 |
$sql = sprintf(
|
| 148 |
'INSERT INTO news (
|
| 149 |
id,
|
|
@@ -200,6 +80,6 @@ final class NewsCrudService
|
|
| 200 |
$viewName
|
| 201 |
);
|
| 202 |
|
| 203 |
-
return (int) $
|
| 204 |
}
|
| 205 |
}
|
|
|
|
| 2 |
|
| 3 |
namespace App\Service;
|
| 4 |
|
|
|
|
|
|
|
| 5 |
use Doctrine\ORM\EntityManagerInterface;
|
| 6 |
|
| 7 |
+
/**
|
| 8 |
+
* Импорт новостей из материализованного представления (Bitrix view).
|
| 9 |
+
*
|
| 10 |
+
* CRUD (create/update/delete/list) живёт теперь в NewsController через
|
| 11 |
+
* общие App\Service\Crud\CrudResponder и App\Service\Pagination\Paginator —
|
| 12 |
+
* этот сервис отвечает только за синхронизацию (см. App\Command\UploadNewsCommand).
|
| 13 |
+
*/
|
| 14 |
final class NewsCrudService
|
| 15 |
{
|
| 16 |
public function __construct(
|
| 17 |
private EntityManagerInterface $em,
|
|
|
|
| 18 |
) {
|
| 19 |
}
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
public function syncFromViewNews(string $viewName = 'public.view_news'): int
|
| 22 |
{
|
| 23 |
if (!preg_match('/^[A-Za-z0-9_\.]+$/', $viewName)) {
|
| 24 |
throw new \InvalidArgumentException('Invalid view name');
|
| 25 |
}
|
| 26 |
|
|
|
|
|
|
|
| 27 |
$sql = sprintf(
|
| 28 |
'INSERT INTO news (
|
| 29 |
id,
|
|
|
|
| 80 |
$viewName
|
| 81 |
);
|
| 82 |
|
| 83 |
+
return (int) $this->em->getConnection()->executeStatement($sql);
|
| 84 |
}
|
| 85 |
}
|
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
declare(strict_types=1);
|
| 4 |
+
|
| 5 |
+
namespace App\Service\Pagination;
|
| 6 |
+
|
| 7 |
+
use Doctrine\ORM\QueryBuilder;
|
| 8 |
+
use Pagerfanta\Doctrine\ORM\QueryAdapter;
|
| 9 |
+
use Pagerfanta\Exception\NotValidCurrentPageException;
|
| 10 |
+
use Pagerfanta\Pagerfanta;
|
| 11 |
+
use Symfony\Component\HttpFoundation\Request;
|
| 12 |
+
|
| 13 |
+
/**
|
| 14 |
+
* Унифицированная обёртка над Pagerfanta + QueryAdapter.
|
| 15 |
+
*
|
| 16 |
+
* Соответствует существующему стилю проекта (см. PriceListController/SpecialistController):
|
| 17 |
+
* читает page/perPage из Request, ограничивает perPage и возвращает массив
|
| 18 |
+
* ['data' => [...], 'pagination' => [...]] в едином формате для новых list-контрактов.
|
| 19 |
+
*/
|
| 20 |
+
final class Paginator
|
| 21 |
+
{
|
| 22 |
+
public const DEFAULT_PER_PAGE = 50;
|
| 23 |
+
public const MAX_PER_PAGE = 500;
|
| 24 |
+
|
| 25 |
+
/**
|
| 26 |
+
* @return array{data: list<mixed>, pagination: array<string, int|bool>}
|
| 27 |
+
*/
|
| 28 |
+
public function paginate(
|
| 29 |
+
QueryBuilder $qb,
|
| 30 |
+
Request $request,
|
| 31 |
+
int $defaultPerPage = self::DEFAULT_PER_PAGE,
|
| 32 |
+
int $maxPerPage = self::MAX_PER_PAGE,
|
| 33 |
+
): array {
|
| 34 |
+
$page = max(1, $request->query->getInt('page', 1));
|
| 35 |
+
$perPage = min(
|
| 36 |
+
max(1, $request->query->getInt('perPage', $defaultPerPage)),
|
| 37 |
+
$maxPerPage,
|
| 38 |
+
);
|
| 39 |
+
|
| 40 |
+
$pagerfanta = (new Pagerfanta(new QueryAdapter($qb)))
|
| 41 |
+
->setMaxPerPage($perPage);
|
| 42 |
+
|
| 43 |
+
try {
|
| 44 |
+
$pagerfanta->setCurrentPage($page);
|
| 45 |
+
} catch (NotValidCurrentPageException) {
|
| 46 |
+
// выходим за пределы — возвращаем пустую страницу с корректным total
|
| 47 |
+
$pagerfanta->setCurrentPage(max(1, $pagerfanta->getNbPages()));
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
$data = iterator_to_array($pagerfanta->getCurrentPageResults(), false);
|
| 51 |
+
|
| 52 |
+
return [
|
| 53 |
+
'data' => $data,
|
| 54 |
+
'pagination' => [
|
| 55 |
+
'total' => $pagerfanta->getNbResults(),
|
| 56 |
+
'count' => count($data),
|
| 57 |
+
'per_page' => $pagerfanta->getMaxPerPage(),
|
| 58 |
+
'current_page' => $pagerfanta->getCurrentPage(),
|
| 59 |
+
'total_pages' => $pagerfanta->getNbPages(),
|
| 60 |
+
'has_previous_page' => $pagerfanta->hasPreviousPage(),
|
| 61 |
+
'has_next_page' => $pagerfanta->hasNextPage(),
|
| 62 |
+
],
|
| 63 |
+
];
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/**
|
| 67 |
+
* Legacy-формат для ArticleController.
|
| 68 |
+
*
|
| 69 |
+
* Старый контракт /article/list уже использовался клиентами:
|
| 70 |
+
* - размер страницы приходит в query-параметре limit;
|
| 71 |
+
* - метаданные лежат в ключе meta;
|
| 72 |
+
* - поля называются total/page/limit/totalPages.
|
| 73 |
+
*
|
| 74 |
+
* @return array{data: list<mixed>, meta: array{total: int, page: int, limit: int, totalPages: int}}
|
| 75 |
+
*/
|
| 76 |
+
public function paginateWithLegacyMeta(
|
| 77 |
+
QueryBuilder $qb,
|
| 78 |
+
Request $request,
|
| 79 |
+
int $defaultLimit = 20,
|
| 80 |
+
int $maxLimit = 100,
|
| 81 |
+
): array {
|
| 82 |
+
$page = max(1, $request->query->getInt('page', 1));
|
| 83 |
+
$limit = min(
|
| 84 |
+
max(1, $request->query->getInt('limit', $defaultLimit)),
|
| 85 |
+
$maxLimit,
|
| 86 |
+
);
|
| 87 |
+
|
| 88 |
+
$pagerfanta = (new Pagerfanta(new QueryAdapter($qb)))
|
| 89 |
+
->setMaxPerPage($limit);
|
| 90 |
+
|
| 91 |
+
try {
|
| 92 |
+
$pagerfanta->setCurrentPage($page);
|
| 93 |
+
} catch (NotValidCurrentPageException) {
|
| 94 |
+
$pagerfanta->setCurrentPage(max(1, $pagerfanta->getNbPages()));
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
return [
|
| 98 |
+
'data' => iterator_to_array($pagerfanta->getCurrentPageResults(), false),
|
| 99 |
+
'meta' => [
|
| 100 |
+
'total' => $pagerfanta->getNbResults(),
|
| 101 |
+
'page' => $pagerfanta->getCurrentPage(),
|
| 102 |
+
'limit' => $pagerfanta->getMaxPerPage(),
|
| 103 |
+
'totalPages' => $pagerfanta->getNbPages(),
|
| 104 |
+
],
|
| 105 |
+
];
|
| 106 |
+
}
|
| 107 |
+
}
|
|
@@ -2,148 +2,26 @@
|
|
| 2 |
|
| 3 |
namespace App\Service;
|
| 4 |
|
| 5 |
-
use App\Entity\Promo;
|
| 6 |
-
use App\Repository\PromoRepository;
|
| 7 |
use Doctrine\ORM\EntityManagerInterface;
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
final class PromoCrudService
|
| 10 |
{
|
| 11 |
public function __construct(
|
| 12 |
private EntityManagerInterface $em,
|
| 13 |
-
private PromoRepository $promoRepository
|
| 14 |
) {
|
| 15 |
}
|
| 16 |
|
| 17 |
-
/**
|
| 18 |
-
* @return Promo[]
|
| 19 |
-
*/
|
| 20 |
-
public function getList(?int $regionId = null, ?bool $active = true): array
|
| 21 |
-
{
|
| 22 |
-
$criteria = [];
|
| 23 |
-
if ($regionId !== null) {
|
| 24 |
-
$criteria['regionId'] = $regionId;
|
| 25 |
-
}
|
| 26 |
-
if ($active !== null) {
|
| 27 |
-
$criteria['active'] = $active;
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
return $this->promoRepository->findBy($criteria, ['id' => 'ASC']);
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
public function getShow(int $id): ?Promo
|
| 34 |
-
{
|
| 35 |
-
return $this->promoRepository->find($id);
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
public function create(array $data): Promo
|
| 39 |
-
{
|
| 40 |
-
$promo = new Promo();
|
| 41 |
-
$this->updateEntity($promo, $data);
|
| 42 |
-
|
| 43 |
-
$this->em->persist($promo);
|
| 44 |
-
$this->em->flush();
|
| 45 |
-
|
| 46 |
-
return $promo;
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
public function update(Promo $promo, array $data): Promo
|
| 50 |
-
{
|
| 51 |
-
unset($data['id']);
|
| 52 |
-
$this->updateEntity($promo, $data);
|
| 53 |
-
|
| 54 |
-
$this->em->flush();
|
| 55 |
-
return $promo;
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
public function delete(Promo $promo): void
|
| 59 |
-
{
|
| 60 |
-
$this->em->remove($promo);
|
| 61 |
-
$this->em->flush();
|
| 62 |
-
}
|
| 63 |
-
|
| 64 |
-
private function updateEntity(Promo $promo, array $data): void
|
| 65 |
-
{
|
| 66 |
-
if (array_key_exists('id', $data) && $data['id'] !== null && $data['id'] !== '') {
|
| 67 |
-
$promo->setId((int) $data['id']);
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
if (array_key_exists('name', $data)) {
|
| 71 |
-
$promo->setName($data['name']);
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
if (array_key_exists('active', $data)) {
|
| 75 |
-
$promo->setActive($data['active']);
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
if (array_key_exists('regionId', $data) || array_key_exists('region_id', $data)) {
|
| 79 |
-
$v = $data['regionId'] ?? $data['region_id'];
|
| 80 |
-
$promo->setRegionId($v === null || $v === '' ? null : (int) $v);
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
if (array_key_exists('alias', $data)) {
|
| 84 |
-
$promo->setAlias($data['alias']);
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
if (array_key_exists('anons', $data)) {
|
| 88 |
-
$promo->setAnons($data['anons']);
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
if (array_key_exists('content', $data)) {
|
| 92 |
-
$promo->setContent($data['content']);
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
if (array_key_exists('updateAt', $data) || array_key_exists('update_at', $data)) {
|
| 96 |
-
$raw = $data['updateAt'] ?? $data['update_at'];
|
| 97 |
-
if ($raw === null || $raw === '') {
|
| 98 |
-
$promo->setUpdateAt(null);
|
| 99 |
-
} elseif ($raw instanceof \DateTimeInterface) {
|
| 100 |
-
$promo->setUpdateAt($raw);
|
| 101 |
-
} elseif (is_string($raw)) {
|
| 102 |
-
$promo->setUpdateAt(new \DateTimeImmutable($raw));
|
| 103 |
-
}
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
-
if (array_key_exists('clinics', $data)) {
|
| 107 |
-
$promo->setClinics($data['clinics']);
|
| 108 |
-
}
|
| 109 |
-
|
| 110 |
-
if (array_key_exists('timer', $data)) {
|
| 111 |
-
$promo->setTimer($data['timer']);
|
| 112 |
-
}
|
| 113 |
-
|
| 114 |
-
if (array_key_exists('timerBg', $data) || array_key_exists('timer_bg', $data)) {
|
| 115 |
-
$promo->setTimerBg($data['timerBg'] ?? $data['timer_bg']);
|
| 116 |
-
}
|
| 117 |
-
|
| 118 |
-
if (array_key_exists('shortName', $data) || array_key_exists('short_name', $data)) {
|
| 119 |
-
$promo->setShortName($data['shortName'] ?? $data['short_name']);
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
if (array_key_exists('linkServices', $data) || array_key_exists('link_services', $data)) {
|
| 123 |
-
$promo->setLinkServices($data['linkServices'] ?? $data['link_services']);
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
if (array_key_exists('linkStaff', $data) || array_key_exists('link_staff', $data)) {
|
| 127 |
-
$promo->setLinkStaff($data['linkStaff'] ?? $data['link_staff']);
|
| 128 |
-
}
|
| 129 |
-
|
| 130 |
-
if (array_key_exists('period', $data)) {
|
| 131 |
-
$promo->setPeriod($data['period']);
|
| 132 |
-
}
|
| 133 |
-
|
| 134 |
-
if (array_key_exists('photos', $data)) {
|
| 135 |
-
$promo->setPhotos($data['photos']);
|
| 136 |
-
}
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
public function syncFromViewPromo(string $viewName = 'public.view_promo'): int
|
| 140 |
{
|
| 141 |
if (!preg_match('/^[A-Za-z0-9_\.]+$/', $viewName)) {
|
| 142 |
throw new \InvalidArgumentException('Invalid view name');
|
| 143 |
}
|
| 144 |
|
| 145 |
-
$connection = $this->em->getConnection();
|
| 146 |
-
|
| 147 |
$sql = sprintf(
|
| 148 |
'INSERT INTO promo (
|
| 149 |
id,
|
|
@@ -200,6 +78,6 @@ final class PromoCrudService
|
|
| 200 |
$viewName
|
| 201 |
);
|
| 202 |
|
| 203 |
-
return (int) $
|
| 204 |
}
|
| 205 |
}
|
|
|
|
| 2 |
|
| 3 |
namespace App\Service;
|
| 4 |
|
|
|
|
|
|
|
| 5 |
use Doctrine\ORM\EntityManagerInterface;
|
| 6 |
|
| 7 |
+
/**
|
| 8 |
+
* Импорт акций из материализованного представления (Bitrix view).
|
| 9 |
+
*
|
| 10 |
+
* См. PromoController + CrudResponder для CRUD; этот сервис — только syncFromView*.
|
| 11 |
+
*/
|
| 12 |
final class PromoCrudService
|
| 13 |
{
|
| 14 |
public function __construct(
|
| 15 |
private EntityManagerInterface $em,
|
|
|
|
| 16 |
) {
|
| 17 |
}
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
public function syncFromViewPromo(string $viewName = 'public.view_promo'): int
|
| 20 |
{
|
| 21 |
if (!preg_match('/^[A-Za-z0-9_\.]+$/', $viewName)) {
|
| 22 |
throw new \InvalidArgumentException('Invalid view name');
|
| 23 |
}
|
| 24 |
|
|
|
|
|
|
|
| 25 |
$sql = sprintf(
|
| 26 |
'INSERT INTO promo (
|
| 27 |
id,
|
|
|
|
| 78 |
$viewName
|
| 79 |
);
|
| 80 |
|
| 81 |
+
return (int) $this->em->getConnection()->executeStatement($sql);
|
| 82 |
}
|
| 83 |
}
|
|
@@ -2,358 +2,26 @@
|
|
| 2 |
|
| 3 |
namespace App\Service;
|
| 4 |
|
| 5 |
-
use App\Entity\SiteService;
|
| 6 |
-
use App\Repository\SiteServiceRepository;
|
| 7 |
use Doctrine\ORM\EntityManagerInterface;
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
final class SiteServiceCrudService
|
| 10 |
{
|
| 11 |
public function __construct(
|
| 12 |
private EntityManagerInterface $em,
|
| 13 |
-
private SiteServiceRepository $siteServiceRepository,
|
| 14 |
) {
|
| 15 |
}
|
| 16 |
|
| 17 |
-
/**
|
| 18 |
-
* @return SiteService[]
|
| 19 |
-
*/
|
| 20 |
-
public function getList(?int $regionId = null, ?bool $active = true): array
|
| 21 |
-
{
|
| 22 |
-
$criteria = [];
|
| 23 |
-
if ($regionId !== null) {
|
| 24 |
-
$criteria['regionId'] = $regionId;
|
| 25 |
-
}
|
| 26 |
-
if ($active !== null) {
|
| 27 |
-
$criteria['active'] = $active;
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
return $this->siteServiceRepository->findBy($criteria, ['id' => 'ASC']);
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
/**
|
| 34 |
-
* @return array{data: SiteService[], total: int, page: int, per_page: int}
|
| 35 |
-
*/
|
| 36 |
-
public function getPaginatedList(int $page, int $perPage, ?int $regionId = null, ?bool $active = true): array
|
| 37 |
-
{
|
| 38 |
-
$page = max(1, $page);
|
| 39 |
-
$perPage = min(max(1, $perPage), 500);
|
| 40 |
-
|
| 41 |
-
$countQb = $this->siteServiceRepository->createQueryBuilder('s')
|
| 42 |
-
->select('COUNT(s.id)');
|
| 43 |
-
if ($regionId !== null) {
|
| 44 |
-
$countQb->andWhere('s.regionId = :regionId')
|
| 45 |
-
->setParameter('regionId', $regionId);
|
| 46 |
-
}
|
| 47 |
-
if ($active !== null) {
|
| 48 |
-
$countQb->andWhere('s.active = :active')
|
| 49 |
-
->setParameter('active', $active);
|
| 50 |
-
}
|
| 51 |
-
$total = (int) $countQb->getQuery()->getSingleScalarResult();
|
| 52 |
-
|
| 53 |
-
$qb = $this->siteServiceRepository->createQueryBuilder('s')
|
| 54 |
-
->orderBy('s.id', 'ASC');
|
| 55 |
-
if ($regionId !== null) {
|
| 56 |
-
$qb->andWhere('s.regionId = :regionId')
|
| 57 |
-
->setParameter('regionId', $regionId);
|
| 58 |
-
}
|
| 59 |
-
if ($active !== null) {
|
| 60 |
-
$qb->andWhere('s.active = :active')
|
| 61 |
-
->setParameter('active', $active);
|
| 62 |
-
}
|
| 63 |
-
$qb->setFirstResult(($page - 1) * $perPage)
|
| 64 |
-
->setMaxResults($perPage);
|
| 65 |
-
|
| 66 |
-
$data = $qb->getQuery()->getResult();
|
| 67 |
-
|
| 68 |
-
return [
|
| 69 |
-
'data' => $data,
|
| 70 |
-
'total' => $total,
|
| 71 |
-
'page' => $page,
|
| 72 |
-
'per_page' => $perPage,
|
| 73 |
-
];
|
| 74 |
-
}
|
| 75 |
-
|
| 76 |
-
public function getShow(int $id): ?SiteService
|
| 77 |
-
{
|
| 78 |
-
return $this->siteServiceRepository->find($id);
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
public function create(array $data): SiteService
|
| 82 |
-
{
|
| 83 |
-
$siteService = new SiteService();
|
| 84 |
-
$this->updateEntity($siteService, $data);
|
| 85 |
-
|
| 86 |
-
$this->em->persist($siteService);
|
| 87 |
-
$this->em->flush();
|
| 88 |
-
|
| 89 |
-
return $siteService;
|
| 90 |
-
}
|
| 91 |
-
|
| 92 |
-
public function update(SiteService $siteService, array $data): SiteService
|
| 93 |
-
{
|
| 94 |
-
unset($data['id']);
|
| 95 |
-
$this->updateEntity($siteService, $data);
|
| 96 |
-
|
| 97 |
-
$this->em->flush();
|
| 98 |
-
|
| 99 |
-
return $siteService;
|
| 100 |
-
}
|
| 101 |
-
|
| 102 |
-
public function delete(SiteService $siteService): void
|
| 103 |
-
{
|
| 104 |
-
$this->em->remove($siteService);
|
| 105 |
-
$this->em->flush();
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
private function updateEntity(SiteService $siteService, array $data): void
|
| 109 |
-
{
|
| 110 |
-
if (array_key_exists('id', $data)) {
|
| 111 |
-
$v = $data['id'];
|
| 112 |
-
$siteService->setId($v === null || $v === '' ? null : (int) $v);
|
| 113 |
-
}
|
| 114 |
-
|
| 115 |
-
if (array_key_exists('name', $data)) {
|
| 116 |
-
$siteService->setName($data['name']);
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
if (array_key_exists('active', $data)) {
|
| 120 |
-
$siteService->setActive($data['active']);
|
| 121 |
-
}
|
| 122 |
-
|
| 123 |
-
if (array_key_exists('regionId', $data) || array_key_exists('region_id', $data)) {
|
| 124 |
-
$v = $data['regionId'] ?? $data['region_id'];
|
| 125 |
-
$siteService->setRegionId($v === null || $v === '' ? null : (int) $v);
|
| 126 |
-
}
|
| 127 |
-
|
| 128 |
-
if (array_key_exists('alias', $data)) {
|
| 129 |
-
$siteService->setAlias($data['alias']);
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
-
if (array_key_exists('anons', $data)) {
|
| 133 |
-
$siteService->setAnons($data['anons']);
|
| 134 |
-
}
|
| 135 |
-
|
| 136 |
-
if (array_key_exists('content', $data)) {
|
| 137 |
-
$siteService->setContent($data['content']);
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
if (array_key_exists('updateAt', $data) || array_key_exists('update_at', $data)) {
|
| 141 |
-
$raw = $data['updateAt'] ?? $data['update_at'];
|
| 142 |
-
if ($raw === null || $raw === '') {
|
| 143 |
-
$siteService->setUpdateAt(null);
|
| 144 |
-
} elseif ($raw instanceof \DateTimeInterface) {
|
| 145 |
-
$siteService->setUpdateAt($raw);
|
| 146 |
-
} elseif (is_string($raw)) {
|
| 147 |
-
$siteService->setUpdateAt(new \DateTimeImmutable($raw));
|
| 148 |
-
}
|
| 149 |
-
}
|
| 150 |
-
|
| 151 |
-
if (array_key_exists('linkVideoreviews', $data) || array_key_exists('link_videoreviews', $data)) {
|
| 152 |
-
$siteService->setLinkVideoreviews($data['linkVideoreviews'] ?? $data['link_videoreviews']);
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
if (array_key_exists('previewImg', $data) || array_key_exists('preview_img', $data)) {
|
| 156 |
-
$siteService->setPreviewImg($data['previewImg'] ?? $data['preview_img']);
|
| 157 |
-
}
|
| 158 |
-
|
| 159 |
-
if (array_key_exists('faq', $data)) {
|
| 160 |
-
$siteService->setFaq($data['faq']);
|
| 161 |
-
}
|
| 162 |
-
|
| 163 |
-
if (array_key_exists('partPrice', $data) || array_key_exists('part_price', $data)) {
|
| 164 |
-
$siteService->setPartPrice($data['partPrice'] ?? $data['part_price']);
|
| 165 |
-
}
|
| 166 |
-
|
| 167 |
-
if (array_key_exists('pokazaniya', $data)) {
|
| 168 |
-
$siteService->setPokazaniya($data['pokazaniya']);
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
if (array_key_exists('preparation', $data)) {
|
| 172 |
-
$siteService->setPreparation($data['preparation']);
|
| 173 |
-
}
|
| 174 |
-
|
| 175 |
-
if (array_key_exists('protivopokazaniya', $data)) {
|
| 176 |
-
$siteService->setProtivopokazaniya($data['protivopokazaniya']);
|
| 177 |
-
}
|
| 178 |
-
|
| 179 |
-
if (array_key_exists('hideSignBtn', $data) || array_key_exists('hide_sign_btn', $data)) {
|
| 180 |
-
$siteService->setHideSignBtn($data['hideSignBtn'] ?? $data['hide_sign_btn']);
|
| 181 |
-
}
|
| 182 |
-
|
| 183 |
-
if (array_key_exists('quiz', $data)) {
|
| 184 |
-
$siteService->setQuiz($data['quiz']);
|
| 185 |
-
}
|
| 186 |
-
|
| 187 |
-
if (array_key_exists('tags', $data)) {
|
| 188 |
-
$siteService->setTags($data['tags']);
|
| 189 |
-
}
|
| 190 |
-
|
| 191 |
-
if (array_key_exists('tagsImportant', $data) || array_key_exists('tags_important', $data)) {
|
| 192 |
-
$siteService->setTagsImportant($data['tagsImportant'] ?? $data['tags_important']);
|
| 193 |
-
}
|
| 194 |
-
|
| 195 |
-
if (array_key_exists('bannerImg', $data) || array_key_exists('banner_img', $data)) {
|
| 196 |
-
$siteService->setBannerImg($data['bannerImg'] ?? $data['banner_img']);
|
| 197 |
-
}
|
| 198 |
-
|
| 199 |
-
if (array_key_exists('bannerImgM', $data) || array_key_exists('banner_img_m', $data)) {
|
| 200 |
-
$siteService->setBannerImgM($data['bannerImgM'] ?? $data['banner_img_m']);
|
| 201 |
-
}
|
| 202 |
-
|
| 203 |
-
if (array_key_exists('bannerImgUrl', $data) || array_key_exists('banner_img_url', $data)) {
|
| 204 |
-
$siteService->setBannerImgUrl($data['bannerImgUrl'] ?? $data['banner_img_url']);
|
| 205 |
-
}
|
| 206 |
-
|
| 207 |
-
if (array_key_exists('clinics', $data)) {
|
| 208 |
-
$siteService->setClinics($data['clinics']);
|
| 209 |
-
}
|
| 210 |
-
|
| 211 |
-
if (array_key_exists('downloadFile', $data) || array_key_exists('download_file', $data)) {
|
| 212 |
-
$siteService->setDownloadFile($data['downloadFile'] ?? $data['download_file']);
|
| 213 |
-
}
|
| 214 |
-
|
| 215 |
-
if (array_key_exists('fullWidthBanner', $data) || array_key_exists('full_width_banner', $data)) {
|
| 216 |
-
$siteService->setFullWidthBanner($data['fullWidthBanner'] ?? $data['full_width_banner']);
|
| 217 |
-
}
|
| 218 |
-
|
| 219 |
-
if (array_key_exists('staffUp', $data) || array_key_exists('staff_up', $data)) {
|
| 220 |
-
$siteService->setStaffUp($data['staffUp'] ?? $data['staff_up']);
|
| 221 |
-
}
|
| 222 |
-
|
| 223 |
-
if (array_key_exists('advantages', $data)) {
|
| 224 |
-
$siteService->setAdvantages($data['advantages']);
|
| 225 |
-
}
|
| 226 |
-
|
| 227 |
-
if (array_key_exists('hidePicture', $data) || array_key_exists('hide_picture', $data)) {
|
| 228 |
-
$v = $data['hidePicture'] ?? $data['hide_picture'];
|
| 229 |
-
$siteService->setHidePicture($v === null || $v === '' ? null : (int) $v);
|
| 230 |
-
}
|
| 231 |
-
|
| 232 |
-
if (array_key_exists('kodUslug', $data) || array_key_exists('kod_uslug', $data)) {
|
| 233 |
-
$siteService->setKodUslug($data['kodUslug'] ?? $data['kod_uslug']);
|
| 234 |
-
}
|
| 235 |
-
|
| 236 |
-
if (array_key_exists('linkPrice', $data) || array_key_exists('link_price', $data)) {
|
| 237 |
-
$siteService->setLinkPrice($data['linkPrice'] ?? $data['link_price']);
|
| 238 |
-
}
|
| 239 |
-
|
| 240 |
-
if (array_key_exists('photosTitle', $data) || array_key_exists('photos_title', $data)) {
|
| 241 |
-
$siteService->setPhotosTitle($data['photosTitle'] ?? $data['photos_title']);
|
| 242 |
-
}
|
| 243 |
-
|
| 244 |
-
if (array_key_exists('saleId', $data) || array_key_exists('sale_id', $data)) {
|
| 245 |
-
$siteService->setSaleId($data['saleId'] ?? $data['sale_id']);
|
| 246 |
-
}
|
| 247 |
-
|
| 248 |
-
if (array_key_exists('sortStaff', $data) || array_key_exists('sort_staff', $data)) {
|
| 249 |
-
$siteService->setSortStaff($data['sortStaff'] ?? $data['sort_staff']);
|
| 250 |
-
}
|
| 251 |
-
|
| 252 |
-
if (array_key_exists('contraindicationsList', $data) || array_key_exists('contraindications_list', $data)) {
|
| 253 |
-
$siteService->setContraindicationsList($data['contraindicationsList'] ?? $data['contraindications_list']);
|
| 254 |
-
}
|
| 255 |
-
|
| 256 |
-
if (array_key_exists('customBlockText', $data) || array_key_exists('custom_block_text', $data)) {
|
| 257 |
-
$siteService->setCustomBlockText($data['customBlockText'] ?? $data['custom_block_text']);
|
| 258 |
-
}
|
| 259 |
-
|
| 260 |
-
if (array_key_exists('customBlockText2', $data) || array_key_exists('custom_block_text2', $data)) {
|
| 261 |
-
$siteService->setCustomBlockText2($data['customBlockText2'] ?? $data['custom_block_text2']);
|
| 262 |
-
}
|
| 263 |
-
|
| 264 |
-
if (array_key_exists('customBlockTitle', $data) || array_key_exists('custom_block_title', $data)) {
|
| 265 |
-
$siteService->setCustomBlockTitle($data['customBlockTitle'] ?? $data['custom_block_title']);
|
| 266 |
-
}
|
| 267 |
-
|
| 268 |
-
if (array_key_exists('customBlockTitle2', $data) || array_key_exists('custom_block_title2', $data)) {
|
| 269 |
-
$siteService->setCustomBlockTitle2($data['customBlockTitle2'] ?? $data['custom_block_title2']);
|
| 270 |
-
}
|
| 271 |
-
|
| 272 |
-
if (array_key_exists('indicationsList', $data) || array_key_exists('indications_list', $data)) {
|
| 273 |
-
$siteService->setIndicationsList($data['indicationsList'] ?? $data['indications_list']);
|
| 274 |
-
}
|
| 275 |
-
|
| 276 |
-
if (array_key_exists('linkArticlesServices', $data) || array_key_exists('link_articles_services', $data)) {
|
| 277 |
-
$siteService->setLinkArticlesServices($data['linkArticlesServices'] ?? $data['link_articles_services']);
|
| 278 |
-
}
|
| 279 |
-
|
| 280 |
-
if (array_key_exists('plusList', $data) || array_key_exists('plus_list', $data)) {
|
| 281 |
-
$siteService->setPlusList($data['plusList'] ?? $data['plus_list']);
|
| 282 |
-
}
|
| 283 |
-
|
| 284 |
-
if (array_key_exists('plusText', $data) || array_key_exists('plus_text', $data)) {
|
| 285 |
-
$siteService->setPlusText($data['plusText'] ?? $data['plus_text']);
|
| 286 |
-
}
|
| 287 |
-
|
| 288 |
-
if (array_key_exists('plusTitle', $data) || array_key_exists('plus_title', $data)) {
|
| 289 |
-
$siteService->setPlusTitle($data['plusTitle'] ?? $data['plus_title']);
|
| 290 |
-
}
|
| 291 |
-
|
| 292 |
-
if (array_key_exists('prepareTitle', $data) || array_key_exists('prepare_title', $data)) {
|
| 293 |
-
$siteService->setPrepareTitle($data['prepareTitle'] ?? $data['prepare_title']);
|
| 294 |
-
}
|
| 295 |
-
|
| 296 |
-
if (array_key_exists('processText', $data) || array_key_exists('process_text', $data)) {
|
| 297 |
-
$siteService->setProcessText($data['processText'] ?? $data['process_text']);
|
| 298 |
-
}
|
| 299 |
-
|
| 300 |
-
if (array_key_exists('processTitle', $data) || array_key_exists('process_title', $data)) {
|
| 301 |
-
$siteService->setProcessTitle($data['processTitle'] ?? $data['process_title']);
|
| 302 |
-
}
|
| 303 |
-
|
| 304 |
-
if (array_key_exists('servicesList', $data) || array_key_exists('services_list', $data)) {
|
| 305 |
-
$siteService->setServicesList($data['servicesList'] ?? $data['services_list']);
|
| 306 |
-
}
|
| 307 |
-
|
| 308 |
-
if (array_key_exists('servicesPhotos', $data) || array_key_exists('services_photos', $data)) {
|
| 309 |
-
$siteService->setServicesPhotos($data['servicesPhotos'] ?? $data['services_photos']);
|
| 310 |
-
}
|
| 311 |
-
|
| 312 |
-
if (array_key_exists('servicesTitle', $data) || array_key_exists('services_title', $data)) {
|
| 313 |
-
$siteService->setServicesTitle($data['servicesTitle'] ?? $data['services_title']);
|
| 314 |
-
}
|
| 315 |
-
|
| 316 |
-
if (array_key_exists('textUp', $data) || array_key_exists('text_up', $data)) {
|
| 317 |
-
$siteService->setTextUp($data['textUp'] ?? $data['text_up']);
|
| 318 |
-
}
|
| 319 |
-
|
| 320 |
-
if (array_key_exists('trainingText', $data) || array_key_exists('training_text', $data)) {
|
| 321 |
-
$siteService->setTrainingText($data['trainingText'] ?? $data['training_text']);
|
| 322 |
-
}
|
| 323 |
-
|
| 324 |
-
if (array_key_exists('whyText', $data) || array_key_exists('why_text', $data)) {
|
| 325 |
-
$siteService->setWhyText($data['whyText'] ?? $data['why_text']);
|
| 326 |
-
}
|
| 327 |
-
|
| 328 |
-
if (array_key_exists('whyTitle', $data) || array_key_exists('why_title', $data)) {
|
| 329 |
-
$siteService->setWhyTitle($data['whyTitle'] ?? $data['why_title']);
|
| 330 |
-
}
|
| 331 |
-
|
| 332 |
-
if (array_key_exists('linkFaq', $data) || array_key_exists('link_faq', $data)) {
|
| 333 |
-
$siteService->setLinkFaq($data['linkFaq'] ?? $data['link_faq']);
|
| 334 |
-
}
|
| 335 |
-
|
| 336 |
-
if (array_key_exists('linkServices', $data) || array_key_exists('link_services', $data)) {
|
| 337 |
-
$siteService->setLinkServices($data['linkServices'] ?? $data['link_services']);
|
| 338 |
-
}
|
| 339 |
-
|
| 340 |
-
if (array_key_exists('linkStaff', $data) || array_key_exists('link_staff', $data)) {
|
| 341 |
-
$siteService->setLinkStaff($data['linkStaff'] ?? $data['link_staff']);
|
| 342 |
-
}
|
| 343 |
-
|
| 344 |
-
if (array_key_exists('photos', $data)) {
|
| 345 |
-
$siteService->setPhotos($data['photos']);
|
| 346 |
-
}
|
| 347 |
-
|
| 348 |
-
}
|
| 349 |
-
|
| 350 |
public function syncFromViewServices(string $viewName = 'public.view_services'): int
|
| 351 |
{
|
| 352 |
-
if (!
|
| 353 |
throw new \InvalidArgumentException('Invalid view name');
|
| 354 |
}
|
| 355 |
|
| 356 |
-
$connection = $this->em->getConnection();
|
| 357 |
$sql = sprintf(
|
| 358 |
'INSERT INTO site_services (
|
| 359 |
id,
|
|
@@ -533,6 +201,6 @@ final class SiteServiceCrudService
|
|
| 533 |
$viewName
|
| 534 |
);
|
| 535 |
|
| 536 |
-
return (int) $
|
| 537 |
}
|
| 538 |
}
|
|
|
|
| 2 |
|
| 3 |
namespace App\Service;
|
| 4 |
|
|
|
|
|
|
|
| 5 |
use Doctrine\ORM\EntityManagerInterface;
|
| 6 |
|
| 7 |
+
/**
|
| 8 |
+
* Импорт услуг из материализованного представления (Bitrix view).
|
| 9 |
+
*
|
| 10 |
+
* См. SiteServiceController + CrudResponder для CRUD; этот сервис — только syncFromView*.
|
| 11 |
+
*/
|
| 12 |
final class SiteServiceCrudService
|
| 13 |
{
|
| 14 |
public function __construct(
|
| 15 |
private EntityManagerInterface $em,
|
|
|
|
| 16 |
) {
|
| 17 |
}
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
public function syncFromViewServices(string $viewName = 'public.view_services'): int
|
| 20 |
{
|
| 21 |
+
if (!preg_match('/^[A-Za-z0-9_.]+$/', $viewName)) {
|
| 22 |
throw new \InvalidArgumentException('Invalid view name');
|
| 23 |
}
|
| 24 |
|
|
|
|
| 25 |
$sql = sprintf(
|
| 26 |
'INSERT INTO site_services (
|
| 27 |
id,
|
|
|
|
| 201 |
$viewName
|
| 202 |
);
|
| 203 |
|
| 204 |
+
return (int) $this->em->getConnection()->executeStatement($sql);
|
| 205 |
}
|
| 206 |
}
|