<?php
namespace App\Controller;
use App\Dto\ProfileFilterFormData;
use App\Dto\ProfileFilterFormDataFilters;
use App\Dto\ProfileListSpecification;
use App\Entity\Location\City;
use App\Form\ProfileFilterForm;
use App\Service\Filter\FilterModeResolver;
use App\Service\Map\ProfileMapFilterService;
use App\Service\ProfileFiltersCatalogBuilder;
use App\Service\ProfileFiltersQueryResolver;
use App\Service\ProfileList;
use App\Service\ProfileListingDataCreator;
use App\Service\ProfileListingRecommendationsFiller;
use App\Service\ProfileFilterService;
use OpenApi\Attributes as OA;
use Porpaginas\Page;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class ProfileFilterController extends AbstractController
{
use ResponseTrait;
public function __construct(
private ProfileListingDataCreator $profileListingDataCreator,
private ProfileFilterService $profilesFilterService,
private ProfileList $profileList,
private ProfileListingRecommendationsFiller $profileListingRecommendationsFiller,
private ProfileFiltersCatalogBuilder $filtersCatalogBuilder,
private ProfileFiltersQueryResolver $filtersQueryResolver,
private FilterModeResolver $filterModeResolver,
private ProfileMapFilterService $profileMapFilterService,
private NormalizerInterface $normalizer,
) {}
#[ParamConverter("city", converter: "city_converter")]
public function form(City $city, Request $request): Response
{
if(!$request->isXmlHttpRequest()) {
throw new BadRequestHttpException();
}
$listing = $request->request->get('listing');
$listingParameters = json_decode($request->request->get('listingParams'), true);
$formData = new ProfileFilterFormData($listing, $listingParameters, new ProfileFilterFormDataFilters());
$listingData = $this->profileListingDataCreator->getListingDataForFilter($listing, $listingParameters, $city);
/** @var ProfileListSpecification $specs */
$specs = $listingData['specs'];
//холостой вызов, чтобы сформировался запрос и записался в свойство ProfileList
$this->profileList->listActiveWithinCityOrderedByStatusWithSpecAvoidingTopPlacement($city, $specs->spec(), $specs->additionalSpecs(), $specs->genders());
$this->profilesFilterService->checkProfilesPresenceByFilters($city, $listingData['listingTypeName']);
$profilePresenceByFilters = $this->profilesFilterService->profilesPresenceByFilters();
$form = $this->createForm(ProfileFilterForm::class, $formData, [
'action' => $this->generateUrl('profile_filters.filter', ['city' => $city->getUriIdentity()]),
'csrf_protection' => false,
'form_data' => $formData,
'profiles_presence_by_filters' => $profilePresenceByFilters,
]);
return $this->prepareForXhr($this->render('ProfileList/filter/form.html.twig', [
'profiles_presence_by_filters' => $profilePresenceByFilters,
'profile_filter_form' => $form->createView(),
]));
}
#[ParamConverter("city", converter: "city_converter")]
public function filter(City $city, Request $request, ProfileFilterService $profilesFilterService): Response
{
if(!$request->isXmlHttpRequest()) {
throw new BadRequestHttpException();
}
$form = $this->createForm(ProfileFilterForm::class, new ProfileFilterFormData('', [], new ProfileFilterFormDataFilters()), [
'action' => $this->generateUrl('profile_filters.filter', ['city' => $city->getUriIdentity()]),
'csrf_protection' => false,
]);
$form->handleRequest($request);
if (false === $form->isSubmitted() || false === $form->isValid()) {
throw new BadRequestHttpException('Form is not submitted');
}
/** @var ProfileFilterFormData $formData */
$formData = $form->getData();
// $specs = $this->profileListSpecificationService->{$listingType}(...$listingArgs);
$listingData = $this->profileListingDataCreator->getListingDataForFilter($formData->listing, $formData->listingParameters, $city);
$specs = $listingData['specs'];
$listingTypeName = $listingData['listingTypeName'];
$result = $profilesFilterService->filter($city, $listingTypeName, $formData->filters, $specs->spec(), $specs->additionalSpecs(), $specs->genders());
$presence = $profilesFilterService->profilesPresenceByFilters();
$recommendationsFill = [
'profiles' => $result,
'applied' => false,
'original_count' => $result instanceof Page ? $result->totalCount() : 0,
];
if ($result instanceof Page) {
$recommendationsFill = $this->profileListingRecommendationsFiller->fill($city, $result, $specs->genders());
}
$form = $this->createForm(ProfileFilterForm::class, $formData, [
'action' => $this->generateUrl('profile_filters.filter', ['city' => $city->getUriIdentity()]),
'csrf_protection' => false,
'form_data' => $formData,
'profiles_presence_by_filters' => $presence,
]);
return $this->prepareForXhr($this->render('ProfileList/profile_filter.profiles.partial.html.twig', [
'profiles' => $recommendationsFill['profiles'],
'recommendationSpec' => \App\Specification\ElasticSearch\ProfileIsLocated::withinCity($city),
'recommendations_fill_applied' => $recommendationsFill['applied'],
'recommendations_fill_original_count' => $recommendationsFill['original_count'],
'show_pagination' => true,
'profiles_presence_by_filters' => $presence,
'profile_filter_form' => $form->createView(),
]));
}
#[Route("/api/profiles/filter", name: "api.profile_filter", methods: ["GET"], format: "json")]
#[OA\Tag(name: "ProfileFilter")]
#[OA\Parameter(name: "page", in: "query", schema: new OA\Schema(type: "integer", default: 1, minimum: 1))]
#[OA\Parameter(name: "mode", in: "query", description: "Response mode: list (default) or map", schema: new OA\Schema(type: "string", enum: ["list", "map"], default: "list"))]
#[OA\Parameter(name: "scale", in: "query", description: "Map zoom level (0–21), used only with mode=map", schema: new OA\Schema(type: "integer", default: 0, minimum: 0, maximum: 21))]
#[OA\Parameter(name: "city", in: "query", description: "City id or uriIdentity (required for search)", schema: new OA\Schema(oneOf: [new OA\Schema(type: "integer"), new OA\Schema(type: "string")]))]
#[OA\Response(
response: 200,
description: 'Filter catalog (no search params) or search results',
content: new OA\JsonContent(
oneOf: [
new OA\Schema(ref: "#/components/schemas/ProfileFiltersCatalog"),
new OA\Schema(ref: "#/components/schemas/ProfileFilteredListingPage"),
new OA\Schema(ref: "#/components/schemas/ProfileFiltersMapResult"),
]
)
)]
#[OA\Response(response: 400, description: 'Search requested without valid city, or invalid mode/scale')]
public function filterApi(Request $request): JsonResponse
{
if (!$this->filtersQueryResolver->hasSearchParams($request)) {
return $this->json([
'filters' => $this->filtersCatalogBuilder->build(),
]);
}
$mode = $this->filterModeResolver->resolve($request);
[$city, $filters, $appliedFilters] = $this->filtersQueryResolver->resolve($request);
if ($mode->isMap()) {
$result = $this->profileMapFilterService->filter($city, $filters, $mode->scale());
$map = $this->normalizer->normalize($result);
if (!is_array($map)) {
$map = ['data' => $map];
}
return $this->json(array_merge(['filters' => $appliedFilters], $map));
}
$listingData = $this->profileListingDataCreator->getListingDataForFilter('listByCity', [], $city);
/** @var ProfileListSpecification $specs */
$specs = $listingData['specs'];
$result = $this->profilesFilterService->filter(
$city,
$listingData['listingTypeName'],
$filters,
$specs->spec(),
$specs->additionalSpecs(),
$specs->genders()
);
$page = $this->normalizer->normalize($result);
if (!is_array($page)) {
$page = ['data' => $page];
}
return $this->json(array_merge(['filters' => $appliedFilters], $page));
}
}