<?php
namespace App\Controller\Frontend;
use App\Api\RMS\SaleEventDataPersister;
use App\Controller\Controller;
use App\Counter\SalesCounter;
use App\Entity\Annotation;
use App\Entity\ExternalSale;
use App\Entity\Sale;
use App\Entity\SearchAlert;
use App\Entity\Vehicle;
use App\Event\FilterBehaviourEvent;
use App\Form\Type\Frontend\PageLimitType;
use App\Form\Type\Frontend\Popin\VehicleCityLocationType;
use App\Form\Type\Frontend\Popin\VehicleEnergyType;
use App\Form\Type\Frontend\Popin\VehicleGroupType;
use App\Form\Type\Frontend\Popin\VehicleMakerType;
use App\Form\Type\Frontend\Popin\VehicleOptionType;
use App\Form\Type\Frontend\Popin\VehicleSegmenttailleType;
use App\Form\Type\Frontend\RecommandVehicleType;
use App\Form\Type\Frontend\VehicleFreeSearchType;
use App\Form\Type\Frontend\VehicleListSortType;
use App\Form\Type\Frontend\VehicleSearchType;
use App\Live\SocketNotifier;
use App\Mailer\Mailer;
use App\Parameter\Provider;
use App\Repository\AuctionRepository;
use App\Repository\CategoryRepository;
use App\Repository\EnergyRepository;
use App\Repository\OptionRepository;
use App\Repository\PurchaseInstructionRepository;
use App\Repository\SaleEventRepository;
use App\Repository\SaleRepository;
use App\Repository\SelectionRepository;
use App\Repository\TransmissionRepository;
use App\Repository\UserRepository;
use App\Repository\VehicleRepository;
use App\Repository\WikiRepository;
use App\Search\AliasHandler;
use App\Search\SearchAlertConverter;
use App\Search\SearchConverter;
use App\Utils\Slugger;
use Doctrine\ORM\EntityManagerInterface;
use Gedmo\Sluggable\Util\Urlizer;
use Knp\Snappy\Pdf;
use Pagerfanta\Adapter\ArrayAdapter;
use Pagerfanta\Pagerfanta;
use Psr\Log\LoggerInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class VehicleController extends Controller
{
private $offsets;
public function __construct(
private readonly SearchConverter $searchConverter,
private readonly AliasHandler $aliasHandler,
private readonly EnergyRepository $energyRepository,
private readonly OptionRepository $optionRepository,
private readonly SaleRepository $saleRepository,
private readonly SaleEventRepository $saleEventRepository,
private readonly VehicleRepository $vehicleRepository,
private readonly TransmissionRepository $transmissionRepository,
private readonly FormFactoryInterface $formFactory,
private readonly SessionInterface $session,
private readonly Mailer $mailer,
int $estimationPriceOffset,
int $argusPriceOffset,
private readonly string $defaultFrom,
private readonly LoggerInterface $saleLogger
) {
$this->offsets = [
'estimation' => $estimationPriceOffset,
'argus' => $argusPriceOffset,
];
}
public function resetCriteriaAction(Request $request)
{
$request->getSession()->remove('freesearch_criteria');
$request->getSession()->remove('form_values');
$request->getSession()->remove('badges');
$request->getSession()->remove('sale');
$request->getSession()->remove('event');
return $this->redirect($this->generateUrl('frontend_vehicle_list'));
}
public function indexAction(Request $request, SelectionRepository $selectionRepository, AuctionRepository $auctionRepository)
{
// session cleanup
if ($this->session->get('sale_results') && !$request->isXmlHttpRequest()) {
// clear all form session values if sales results list was previously accessed (remove interaction)
$this->clearSearchFormSession($request->getSession(), false);
// unset flag
$request->getSession()->remove('sale_results');
}
$saleId = $this->session->get('sale', null);
$eventId = $this->session->get('event', null);
$currentSale = null !== $saleId ? $this->saleRepository->findOneById($saleId) : null;
$currentEvent = $eventId ?? null;
$limitForm = $this->formFactory->create(PageLimitType::class);
$limitForm->handleRequest($request);
if ($limitForm->isSubmitted() && $limitForm->isValid()) {
$this->session->set('max_per_page', $limitForm['nbItems']->getData());
}
if ($request->isXmlHttpRequest()) {
if (null !== $request->query->get('page', null)) {
$this->searchConverter->convertToDBCriteria($this->session->get('form_values'));
} else {
$search = $request->query->all();
if (!isset($search['sale'])) {
$this->session->set('sale', null);
}
if (!isset($search['event'])) {
$this->session->set('event', null);
}
$this->session->set('form_values', $search);
$this->session->set('badges', $this->searchConverter->convertToDisplayableValues($search));
$this->searchConverter->convertToDBCriteria($search);
}
return new Response();
}
$freeSearch = $this->session->get('freesearch_criteria');
$criteria = [];
if (null !== $freeSearch) {
$criteria = $freeSearch;
$this->session->set('form_values', $criteria['form']);
$this->session->set('badges', $this->searchConverter->convertToDisplayableValues($criteria['form']));
unset($criteria['form']);
$request->getSession()->remove('freesearch_criteria');
} elseif (null !== $this->session->get('form_values', null)) {
$criteria = $this->searchConverter->convertToDBCriteria($this->session->get('form_values'));
}
if (
isset($search['estimated'])
|| (isset($criteria['estimated']) && $criteria['estimated'])
) {
$estimated = true;
} else {
$estimated = false;
}
$user = $this->getUser();
$pager = $this->vehicleRepository->getPagerVehicles(
$criteria,
$user,
$estimated,
$this->offsets,
$currentSale,
$this->session->get('dimension', 'private')
);
$pager->setMaxPerPage($this->session->get('max_per_page', Vehicle::PAGER_MAX));
$currentPage = $request->query->get('page', 1);
if ($pager->getNbPages() < $request->query->get('page', 1)) {
if ($pager->getNbResults() > 0) {
$currentPage = $pager->getNbPages();
}
}
$pager->setCurrentPage($currentPage);
if (null !== $user) {
$selections = $selectionRepository->findActiveVehicleIdsByUser($user);
} else {
$selections = $this->session->get('selections', []);
}
$sortForm = $this->formFactory->create(VehicleListSortType::class, null, [
'currentSale' => $currentSale,
]);
$nbVehiclesSales = 0;
if (null === $user && 'private' === $this->session->get('dimension', 'private')) {
$nbVehiclesSales = $this->saleRepository->countVehiclesForActiveByUserAndCustomer($user);
$sales = $this->saleRepository->findActiveByUserAndCustomer($user);
} else {
$sales = $this->saleRepository->findActiveByUser($user);
}
$this->session->set('sale', $currentSale);
$datesForCurrentSale = [];
if ($currentSale instanceof ExternalSale && SaleEventDataPersister::PROVIDER_NAME === $currentSale->getProvider()) {
$dates = $auctionRepository->findMinAndMaxClosedAtForSale($currentSale);
$dates = array_shift($dates);
if ($dates['max_closedAt'] != $dates['min_closedAt']) {
$datesForCurrentSale = ['max_closedAt' => new \DateTime($dates['max_closedAt']), 'min_closedAt' => new \DateTime($dates['min_closedAt'])];
}
}
return $this->render('Frontend/vehicle/index.html.twig', [
'pager' => $pager,
'nbVehiclesSales' => $nbVehiclesSales,
'sales' => $sales,
'currentSale' => $currentSale,
'currentEvent' => $currentEvent,
'limitForm' => $limitForm->createView(),
'sortForm' => $sortForm->createView(),
'selections' => $selections,
'showRegistrationBanner' => !$request->cookies->has('registration-banner'),
'listType' => null !== $user ? $user->getVehicleListType() : $this->session->get('vehicle_list_type', null),
'datesForCurrentSale' => $datesForCurrentSale,
]);
}
public function resultsAction(Request $request, UserRepository $userRepository, Provider $parameterProvider)
{
// session cleanup
if (!$this->session->get('sale_results')) {
// first access to results page
// clear all form session values for a fresh start
$this->clearSearchFormSession($request->getSession());
// set flag
$this->session->set('sale_results', true);
}
// Limit form
$limitForm = $this->formFactory->create(PageLimitType::class, null, ['show_96_choice' => false]);
$limitForm->handleRequest($request);
if ($limitForm->isSubmitted() && $limitForm->isValid()) {
$this->session->set('max_per_page', $limitForm['nbItems']->getData());
}
// $pagerMax = $this->session->get('max_per_page', Vehicle::PAGER_MAX);
$pagerMax = Vehicle::RESULTS_MAX;
// search criteria
if ($request->isXmlHttpRequest()) {
$this->session->set('form_values', $request->query->all());
return new JsonResponse(true);
}
$criteria = $this->searchConverter->convertToDBCriteria($this->session->get('form_values'), VehicleSearchType::FORM_SALE_RESULTS);
$excluded_winners = [];
$guaranteeUser = $userRepository->findOneByEmail($parameterProvider->getParameterValue('vpautopro.guarantee.user.email'));
if ($guaranteeUser) {
$excluded_winners = [$guaranteeUser];
}
/**
* Get only 48 sold vehicules.
*
* @see Vehicle::RESULTS_MAX for the limit
*/
$qb = $this->vehicleRepository->getQueryBuilder($criteria, null, true, [], Vehicle::RESULTS_MAX, VehicleSearchType::FORM_SALE_RESULTS, null, 'private', $excluded_winners);
// If we use “$pager = $this->vehicleRepository->getPaginator($qb);”,
// the limit of 48 won't be applied on results counter, so we fetch data and use an ArrayAdapter
$result = $qb->getQuery()->getResult();
$adapter = new ArrayAdapter($result);
// pager
$pager = new Pagerfanta($adapter);
$pager->setMaxPerPage($pagerMax);
$currentPage = $request->query->get('page', 1);
if ($pager->getNbPages() < $currentPage) {
$currentPage = $pager->getNbPages();
}
$pager->setCurrentPage($currentPage);
$sortForm = $this->formFactory->create(VehicleListSortType::class, null);
return $this->render('Frontend/vehicle/results.html.twig', [
'pager' => $pager,
'limitForm' => $limitForm->createView(),
'selections' => [],
'displayAsResult' => true,
'sortForm' => $sortForm->createView(),
]);
}
public function storeSearchAction(Request $request, SaleEventRepository $saleEventRepository)
{
// Request coming from the homepage's form (with free search)
if ('POST' === $request->getMethod()) {
$form = $this->formFactory->create(VehicleFreeSearchType::class, null, ['user' => $this->getUser()]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$maxKm = $form->get('maxKm')->getData() ? (' '.$form->get('maxKm')->getData().'km') : '';
$maxPrice = $form->get('maxPrice')->getData() ? (' '.$form->get('maxPrice')->getData().'euros') : '';
$criteria = $this->handleFreeSearch(
$form->get('freeSearch')->getData().$maxKm.$maxPrice,
$form->get('sale')->getData()
);
$badges = $this->searchConverter->convertToDisplayableValues($criteria['form']);
$this->session->set('freesearch_criteria', $criteria);
$this->session->set('form_values', $criteria['form']);
$this->session->set('badges', $badges);
$this->session->set('sale', $form->get('sale')->getData());
}
} elseif ('GET' === $request->getMethod()) {
$search = str_replace('-', ' ', $request->attributes->get('terms', ''));
$sale = null;
$event = null;
$request->getSession()->remove('freesearch_criteria');
$request->getSession()->remove('form_values');
$request->getSession()->remove('badges');
$request->getSession()->remove('sale');
$request->getSession()->remove('event');
if ($saleData = $request->attributes->get('sale', false)) {
if (null !== $sale = $this->saleRepository->findVeryNextPhysicalSaleByName($saleData)) {
$this->denyAccessUnlessGranted('view', $sale, 'test');
$this->session->set('sale', $sale);
} elseif (
(null !== $sale = $this->saleRepository->findOneById($saleData))
&& in_array($sale, $this->saleRepository->findAllActive())
) {
$this->session->set('sale', $sale);
} else {
$sale = -1;
}
}
// Fix for unlogged users reaching the vehicle list from an email invitation link, without going through homepage
if ($sale instanceof Sale && !$request->getSession()->has('dimension')) {
$request->getSession()->set('dimension', Sale::TYPE_ONLINE === $sale->getType() ? 'pro' : 'private');
}
if ($eventData = $request->attributes->get('event', false)) {
if (null !== $event = $saleEventRepository->findOneById($eventData)) {
if (in_array($event->getSale(), $this->saleRepository->findAllActive())) {
$this->session->set('sale', $event->getSale());
$this->session->set('event', $event->getId());
}
}
}
$criteria = $this->handleFreeSearch($search, $sale, $event ? $event->getId() : null);
$this->session->set('freesearch_criteria', $criteria);
$this->session->set('form_values', $criteria['form']);
$this->session->set('badges', $this->searchConverter->convertToDisplayableValues($criteria['form']));
}
return $this->redirect($this->generateUrl('frontend_vehicle_list'));
}
public function storeSearchFromAlertAction(Request $request, SearchAlertConverter $searchAlertConverter, $alertId, EntityManagerInterface $em)
{
/** @var SearchAlert $searchAlert */
$searchAlert = $this->findEntityOr404(SearchAlert::class, [
'id' => $alertId,
], $em);
$formValues = $searchAlertConverter->convertFromSearchAlert($searchAlert);
$this->session->set('form_values', $formValues);
$this->session->set('badges', $this->searchConverter->convertToDisplayableValues($formValues));
return $this->redirect($this->generateUrl('frontend_vehicle_list'));
}
public function searchAction(Request $request, CategoryRepository $categoryRepository, $formType = VehicleSearchType::FORM_DEFAULT)
{
$categories = $categoryRepository->findAll();
foreach ($categories as $key => $category) {
$categories[$category->getNameCanonical()] = $category->getId();
unset($categories[$key]);
}
return $this->render('/Frontend/search.html.twig', [
'freesearch_form' => $this->formFactory->create(VehicleFreeSearchType::class)->createView(),
'search_form' => $this->getSearchForm($request->getSession()->get('form_values', []), ['form_type' => $formType])->createView(),
'categories' => $categories,
'form_values' => $this->session->get('form_values'),
'form_type' => $formType,
]);
}
public function removeCriteriaAction(Request $request, EnergyRepository $energyRepository, $field, $value)
{
$values = $this->session->get('form_values', []);
if ('sale' === $field) {
$request->getSession()->remove('sale');
$request->getSession()->remove('event');
if (isset($values['event'])) {
unset($values['event']);
}
}
if ('event' === $field) {
$request->getSession()->remove('event');
}
// Need to reduce keys so that i.e. 'year' empties 'yearMin' AND 'yearMax'
foreach ($values as $key => $v) {
if (
sprintf('%sMin', $field) === $key
|| sprintf('%sMax', $field) === $key
|| sprintf('%sOffset', $field) === $key
) {
$values[$field] = $values[$key];
unset($values[$key]);
}
}
// special case
if ('energy' === $field
&& ($energy = $energyRepository->find($value))) {
$value = $energy->getName();
}
if ('driveWheel' === $field) {
unset($values['fourwheeldrive']);
}
// We either want to remove a value among others, or empty the field if there's only one value
if (isset($values[$field])) {
if (is_string($values[$field]) && str_contains($values[$field], ',')) {
$valueCanonical = trim(strtolower((string) $value));
$removeValueFct = fn ($val) => trim(strtolower((string) $val)) !== $valueCanonical;
$values[$field] = implode(',', array_filter(explode(',', $values[$field]), $removeValueFct));
} else {
unset($values[$field]);
}
}
$this->session->set('form_values', $values);
$this->session->set('badges', $this->searchConverter->convertToDisplayableValues($values));
return $this->redirect($request->headers->get('referer') ?? $this->generateUrl('frontend_vehicle_list'));
}
// Vehicle accept a null value in order to avoid exception:
// “VPAutoBundle\Entity\Vehicle object not found by the ParamConverter annotation.”
public function viewAction(Request $request,
EntityManagerInterface $entityManager,
SocketNotifier $socketNotifier,
Provider $parameterProvider,
SelectionRepository $selectionRepository,
PurchaseInstructionRepository $purchaseInstructionRepository,
WikiRepository $wikiRepository,
EventDispatcherInterface $eventDispatcher,
?Vehicle $vehicle)
{
// ToDo: code should be deleted after optimization
// $startTime = microtime(true);
if (!($vehicle instanceof Vehicle)) {
throw new NotFoundHttpException();
}
$this->denyAccessUnlessGranted('view', $vehicle);
$eventDispatcher->dispatch(new FilterBehaviourEvent($this->getUser(), $vehicle), FilterBehaviourEvent::VEHICLE_BEHAVIOUR);
$sale = $vehicle->getEvent()->getSale();
if (null === $sale) {
$request->getSession()->getFlashBag()->add('error', 'frontend.vehicle.view.notfound.flash.error');
return $this->redirect($this->generateUrl('frontend_vehicle_list'));
}
$this->session->set('vp_auto_active_sale_type', $vehicle->getEvent()->getSale()->getType());
// For statistics
$socketNotifier->disable();
$vehicle->addOneToNbViews();
$entityManager->flush();
// Criteo: get the user id
$userId = $this->getUser() ? $this->getUser()->getId() : null;
$bodyDetails = $parameterProvider->getParameterValue('vehicle.body_details', 'bool');
if (null !== $this->getUser()) {
$selection = $selectionRepository->findOneBy([
'user' => $this->getUser(),
'vehicle' => $vehicle,
]);
} else {
$selection = in_array($vehicle->getId(), $this->session->get('selections', [])) ?: null;
}
$pi = null;
if ($this->session->get('pi.success', false)) {
$pi = $purchaseInstructionRepository->findOneByUserAndVehicle($this->getUser(), $vehicle);
$request->getSession()->remove('pi.success');
}
$wikis = array_filter([
'maker' => $wikiRepository->findOneBy(['reference' => $vehicle->getMaker(), 'published' => true]),
'model' => $wikiRepository->findOneByMakerAndReferenceInModel($vehicle->getMaker(), $vehicle->getModelGroup()),
]);
$sales = $this->saleRepository->findActiveByUser($this->getUser());
$annotation = $entityManager->getRepository(Annotation::class)->findOneBy([
'user' => $this->getUser(),
'vehicle' => $vehicle,
]);
// ToDo: code should be deleted after optimization
// $endTime = microtime(true);
// $executionTime = $endTime - $startTime;
// $this->saleLogger->debug('viewAction execution time is : '.$executionTime.' seconds.');
return $this->render('Frontend/vehicle/view.html.twig', [
'bodyDetails' => $bodyDetails,
'vehicle' => $vehicle,
'selection' => $selection,
'userId' => $userId,
'sales' => $sales,
'currentSale' => $sale,
'pi' => $pi,
'wikis' => $wikis,
'annotation' => $annotation,
]);
}
public function printAction(Vehicle $vehicle)
{
return $this->render(
'Frontend/vehicle/pdf_view.html.twig',
[
'vehicle' => $vehicle,
'nextVehicle' => null,
'prevVehicle' => null,
]
);
}
public function makerAutocompleteAction(Request $request)
{
if (!$request->isXmlHttpRequest()) {
throw new AccessDeniedHttpException('Request must be ajax');
}
$term = $request->request->get('term');
$formType = $this->isReferer($this->generateUrl('frontend_vehicle_results', [], UrlGeneratorInterface::ABSOLUTE_URL)) ?
VehicleSearchType::FORM_SALE_RESULTS : VehicleSearchType::FORM_DEFAULT;
$criteria = $this->searchConverter->convertToDBCriteria($this->session->get('form_values'), $formType);
unset($criteria['makerSlug']);
unset($criteria['modelGroupSlug']);
unset($criteria['sortedby']);
$results = $this->vehicleRepository->getAutocompleteMakerQuery($term, $criteria, $formType);
return new JsonResponse($results);
}
public function pdfAction(Request $request, Pdf $pdf, $id, $slug)
{
$vehicle = $this->findVehicle($request, $id);
if (null === $vehicle) {
return $this->redirect($this->generateUrl('frontend_vehicle_list'));
}
$html = $this->renderView(
'Frontend/vehicle/pdf_view.html.twig',
[
'vehicle' => $vehicle,
'nextVehicle' => null,
'prevVehicle' => null,
]
);
return new Response(
$pdf->getOutputFromHtml($html),
200,
[
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="vehicle.pdf"',
]
);
}
public function filteredListPdfAction(Pdf $pdf, TranslatorInterface $translator)
{
$criteria = $this->searchConverter->convertToDBCriteria($this->session->get('form_values'));
if (isset($criteria['estimated']) && $criteria['estimated']) {
$estimated = true;
} else {
$estimated = false;
}
$vehicles = $this->vehicleRepository
->getQueryBuilder($criteria, $this->getUser(), $estimated, $this->offsets)
->getQuery()
->getResult();
$html = $this->renderView(
'/Pdf/filtered_list.html.twig',
[
'vehicles' => $vehicles,
]
);
return new Response(
$pdf->getOutputFromHtml($html),
200,
[
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename='.$translator->trans('pdf.export.search.filename').'.pdf',
]
);
}
public function compareAction(Request $request, TranslatorInterface $translator, Pdf $pdf)
{
$ids = json_decode((string) $request->get('vp_auto_vehicle_select', []));
$vehicles = [];
if (count($ids) > 0) {
foreach ($ids as $id) {
$vehicles[] = $this->findVehicle($request, $id);
}
$options = [];
foreach ($vehicles as $vehicle) {
$vehicleOptions = [];
foreach ($vehicle->getOptions() as $vehicleOption) {
$vehicleOptions[] = '<p>'.$vehicleOption->getName().'</p>';
}
$vehicleOptions = implode('', $vehicleOptions);
$options[$vehicle->getId()] = [
$translator->trans('frontend.vehicle.view.saleDate') => $vehicle->getEvent()->getSale()->getStartDate() ? $vehicle->getEvent()->getSale()->getStartDate()->format('d/m/Y') : 'N/C',
$translator->trans('frontend.vehicle.view.saleLocation') => $vehicle->getEvent()->getSale()->getRoom()->getName(),
$translator->trans('frontend.vehicle.index.order') => $vehicle->getLot(),
$translator->trans('frontend.vehicle.view.folderId') => $vehicle->getEtincelleId(),
$translator->trans('frontend.vehicle.index.makerAndGroup') => $vehicle->getMaker(),
$translator->trans('frontend.vehicle.estimate.form.version') => $vehicle->getModelGroup(),
$translator->trans('frontend.vehicle.information.type') => $vehicle->getClass(),
$translator->trans('frontend.vehicle.estimate.form.first_registration') => $vehicle->getRegistrationDate() ? $vehicle->getRegistrationDate()->format('d/m/Y') : 'N/C',
$translator->trans('frontend.vehicle.view.mileage') => $vehicle->getKilometers().' km',
$translator->trans('frontend.vehicle.view.vehicleBody') => $vehicle->getSegmentBody(),
$translator->trans('frontend.vehicle.view.energy') => $vehicle->getEnergy() ? $vehicle->getEnergy()->getName() : 'NC',
$translator->trans('frontend.vehicle.view.horsepower') => $vehicle->getTaxHorsePower(),
$translator->trans('frontend.vehicle.information.gearbox') => $vehicle->getGearbox(),
'Norme euro' => $vehicle->getValueOfOption('normeeuro'),
'Couleur' => $vehicle->getColor() ? $vehicle->getColor()->getName() : 'NC',
'Nb. places' => $vehicle->getValueOfOption('places'),
'Equipement' => $vehicleOptions,
$translator->trans('frontend.vehicle.view.estimate') => 0 != $vehicle->getValueOfOption('cpquote') ? $vehicle->getValueOfOption('cpquote') : '',
$translator->trans('frontend.vehicle.index.price') => $vehicle->getStartingPrice().' €', ];
if ('' == $options[$vehicle->getId()][$translator->trans('frontend.vehicle.view.estimate')]) {
$options[$vehicle->getId()][$translator->trans('frontend.vehicle.index.price')] = '';
}
}
if ($request->isXmlHttpRequest()) {
return $this->render('Frontend/vehicle/compare.html.twig', [
'ids' => $ids,
'vehicles' => $vehicles,
'firstvehicle' => $vehicle,
'options' => $options,
]);
}
return new Response(
$pdf->getOutputFromHtml(
$this->renderView(
'Frontend/vehicle/compare.html.twig', [
'vehicles' => $vehicles,
'firstvehicle' => $vehicle,
'options' => $options,
])
),
200,
[
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="comparatif.pdf"',
]
);
}
return new Response('', 500);
}
public function getMakersAction(Request $request, $formType = VehicleSearchType::FORM_DEFAULT)
{
return $this->handleAjaxSearchForm($request, $formType, VehicleMakerType::class, 'maker', 'maker');
}
public function getCityLocationsAction(Request $request, $formType = VehicleSearchType::FORM_DEFAULT)
{
if ($request->isXmlHttpRequest()) {
$form = $this->formFactory->create(
VehicleCityLocationType::class,
[],
['form_type' => intval($formType)]
);
return $this->render('Frontend/vehicle/popin_search.html.twig', [
'search_form' => $form->createView(),
]);
}
throw new AccessDeniedHttpException('Request must be ajax');
}
public function getModelsAction(Request $request, $formType = VehicleSearchType::FORM_DEFAULT)
{
return $this->handleAjaxSearchForm($request, $formType, VehicleGroupType::class, 'group', 'modelGroup');
}
public function getSegmentTaillesAction(Request $request, $formType = VehicleSearchType::FORM_DEFAULT)
{
return $this->handleAjaxSearchForm($request, $formType, VehicleSegmenttailleType::class, 'segmenttaille', 'segmentSize');
}
/**
* Method called through Ajax.
*
* @return Response The corresponding options
*/
public function getOptionsAction(Request $request)
{
if ($request->isXmlHttpRequest()) {
$form = $this->formFactory->create(VehicleOptionType::class);
return $this->render('Frontend/vehicle/popin_search.html.twig', [
'search_form' => $form->createView(),
]);
}
throw new AccessDeniedHttpException();
}
public function getEnergiesAction(Request $request, $formType = VehicleSearchType::FORM_DEFAULT)
{
return $this->handleAjaxSearchForm($request, $formType, VehicleEnergyType::class, 'energy', 'energy');
}
public function similarAction(Vehicle $vehicle)
{
$vehicles = $this->vehicleRepository->findSimilarVehicles($vehicle);
return $this->render('Frontend/vehicle/similar.html.twig', [
'vehicles' => $vehicles,
]);
}
// Used for the first footer column.
public function getTopMakersAction()
{
$sales = $this->saleRepository->findActiveByUser($this->getUser());
$vehicles = $this->vehicleRepository->findTopMakersByNbViews(8, $sales);
$items = [];
foreach ($vehicles as $v) {
$imageMaker = str_replace(' ', '-', strtolower((string) $v['maker'])).'.webp';
$items[] = [
'maker' => $v['makerSlug'],
'group' => null,
'name' => $this->truncateText(ucfirst(strtolower((string) $v['maker'])), 7),
'image' => $imageMaker,
'cnt' => $this->vehicleRepository->countByMakerAndModel($v['maker'], $sales),
];
}
return $this->render('frontend/footer_lists.html.twig', [
'items' => $items,
'breakpoint' => 4,
'images' => true,
'horizontal' => true,
]);
}
// Used for the second footer column.
public function getTopModelsAction()
{
$sales = $this->saleRepository->findActiveByUser($this->getUser());
$vehicles = $this->vehicleRepository->findTopModelsByNbViews(10, $sales);
$items = [];
foreach ($vehicles as $v) {
$items[] = [
'maker' => $v['makerSlug'],
'group' => $v['modelGroupSlug'],
'name' => $this->truncateText($v['modelGroup'], 15),
'cnt' => $this->vehicleRepository->countByMakerAndModel($v['maker'], $sales, $v['modelGroup']),
];
}
return $this->render('frontend/footer_lists.html.twig', [
'items' => $items,
'breakpoint' => 5,
'images' => false,
'horizontal' => false,
]);
}
public function recommandAction(Request $request, TranslatorInterface $translator, Vehicle $vehicle)
{
$form = $this->formFactory->create(RecommandVehicleType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
$this->mailer->send(
$this->renderView(
'/Mail/recommand.html.twig',
[
'data' => $data,
'vehicle' => $vehicle,
]
),
'[VPAuto] '.$data['senderName'].' vous recommande ce véhicule',
$data['recipientEmail'],
$this->defaultFrom
);
$this->session->getFlashbag()->add('success', $translator->trans('frontend.vehicle.recommand.success'));
return $this->redirect($this->generateUrl('frontend_vehicle_view', [
'id' => $vehicle->getId(),
'slug' => $vehicle->getSlug(),
]));
}
return $this->render('Frontend/vehicle/recommand.html.twig', [
'vehicle' => $vehicle,
'form' => $form->createView(),
]);
}
public function switchListTypeAction(Request $request, EntityManagerInterface $entityManager, $type)
{
$user = $this->getUser();
if (!in_array($type, ['Liste', 'Mosaique'])) {
throw new \Exception('Unknown list type');
}
if (null !== $user) {
$user->setVehicleListType($type);
$entityManager->flush();
} else {
$this->session->set('vehicle_list_type', $type);
}
if (null !== $request->headers->get('referer')) {
return $this->redirect($request->headers->get('referer'));
}
return $this->redirect($this->generateUrl('frontend_vehicle_list'));
}
public function view3dAction(Vehicle $vehicle)
{
return $this->render('/Frontend/landing/index-3d.html.twig', [
'vehicle' => $vehicle,
]);
}
public function salesCounterAction(SalesCounter $salesCounter)
{
$salesCount = $salesCounter->getSalesCount();
return $this->render('Frontend/vehicle/sales_counter.html.twig', [
'salesCount' => $salesCount,
]);
}
private function handleAjaxSearchForm(Request $request, $formType, $formClass, string $queryKey, string $sessionKey)
{
if ($request->isXmlHttpRequest()) {
if (VehicleSearchType::FORM_ALERT_RESULTS == $formType) {
$criteria = $this->session->get('searchalert_values');
$criteria[$sessionKey] = $request->query->get($queryKey);
$this->session->set('searchalert_values', $criteria);
}
$form = $this->formFactory->create(
$formClass,
[],
['form_type' => intval($formType)]
);
return $this->render('Frontend/vehicle/popin_search.html.twig', [
'search_form' => $form->createView(),
]);
}
throw new AccessDeniedHttpException('Request must be ajax');
}
private function getSearchForm(array $values = [], array $options = [])
{
$form = $this->formFactory->create(VehicleSearchType::class, null, $options);
$form->setData($values);
return $form;
}
/**
* This method handles the free search form on the homepage
* You can consider it a V1, dirty and non-optimized, but hey, it works.
*
* @param string $freeSearch The sentence-like search
* @param string $saleId The sale's id
* @param mixed|null $eventId
*
* @return array Criterias for the search
*/
private function handleFreeSearch($freeSearch, $saleId = null, $eventId = null)
{
$criteria = [];
$criteria['form'] = [];
$found = [];
if (!empty($saleId)) {
if (null !== $sale = $this->saleRepository->findOneById($saleId)) {
$criteria['sale'] = $sale;
$criteria['form']['sale'] = $sale;
if ($sale instanceof ExternalSale && SaleEventDataPersister::PROVIDER_NAME === $sale->getProvider()) {
$criteria['sortedby'] = 'a.closedAt:ASC';
$criteria['form']['sort'] = 'a.closedAt:ASC';
}
} else {
$criteria['sale'] = -1;
}
}
if (
!empty($eventId)
&& null !== $event = $this->saleEventRepository->findOneById($eventId)
) {
$criteria['event'] = $eventId;
$criteria['form']['event'] = $eventId;
}
if (null === $freeSearch or '' === $freeSearch) {
return $criteria;
}
// Let's... handle... the free search...
/**
* This will replace synonyms by their db equivalent (ex: "Diesel" becomes "Gazole")
* Why is this here? Well, we're comparing user input to exact, fixed words, so...
*
* Check the search_alias table for current content / adding new aliases
*/
$freeSearch = $this->aliasHandler->handle($freeSearch);
/*
* This will check if any of the terms are enclosed with "",
* meaning they represent a single term and should be read as such
* The trick is to replace any space within with a dash
*/
preg_match_all('#"[\w\s-]+"#', $freeSearch, $matches);
foreach ($matches[0] as $compound) {
$freeSearch = str_replace($compound, str_replace(['"', ' '], ['', '-'], $compound), $freeSearch);
}
/*
* Pattern for: "de 1000 euros à 2000 euros", "de 1000€ à 2000€"...
*/
if (preg_match('#de\s([0-9]+)\s?(?:€|euros?)?\s(?:a|à)\s([0-9]+)\s?(?:€|euros?)#i', $freeSearch, $matches)) {
$criteria['price']['min'] = (int) $matches[1];
$criteria['price']['max'] = (int) $matches[2];
$criteria['form']['budget']['min'] = (int) $matches[1];
$criteria['form']['budget']['max'] = (int) $matches[2];
$criteria['form']['budgetMax'] = (int) $matches[2];
$found['budget'] = true;
$freeSearch = str_replace($matches[0], '', $freeSearch);
$freeSearch = str_replace(' ', ' ', $freeSearch);
}
/*
* Pattern for: "1000€", "1000euros", "1000 euro"...
*/
if (!isset($found['budget']) && preg_match('#([0-9]+)\s?(?:€|euros?)#i', $freeSearch, $matches)) {
$criteria['price']['min'] = 0;
$criteria['price']['max'] = (int) $matches[1];
$criteria['form']['budget']['min'] = 0;
$criteria['form']['budget']['max'] = (int) $matches[1];
$criteria['form']['budgetMax'] = (int) $matches[1];
$found['budget'] = true;
$freeSearch = str_replace($matches[0], '', $freeSearch);
$freeSearch = str_replace(' ', ' ', $freeSearch);
}
/*
* Pattern for: "50000km", "50000 kilomètres", "50000 kms"...
*/
if (preg_match('#([0-9]+)\s?k(?:ilo)?m(?:(?:e|è)tre)?s?#i', $freeSearch, $matches)) {
$criteria['kilometers']['min'] = 0;
$criteria['kilometers']['max'] = (int) $matches[1];
$criteria['form']['kilometers']['min'] = 0;
$criteria['form']['kilometers']['max'] = (int) $matches[1];
$criteria['form']['kilometersMax'] = (int) $matches[1];
$found['kilometers'] = true;
$freeSearch = str_replace($matches[0], '', $freeSearch);
$freeSearch = str_replace(' ', ' ', $freeSearch);
}
/*
* Pattern for: "#564512"...
*/
if (preg_match('/\#([0-9]+)/i', $freeSearch, $matches)) {
$criteria['etincelleId'] = (int) $matches[1];
$found['etincelleId'] = true;
$freeSearch = str_replace($matches[0], '', $freeSearch);
$freeSearch = str_replace(' ', ' ', $freeSearch);
}
/*
* Pattern for: "lot(s) phare(s)"
*/
if (preg_match('/lots? phares?/i', $freeSearch, $matches)) {
$criteria['highlight'] = true;
$criteria['form']['highlight'] = true;
$found['highlight'] = true;
$freeSearch = str_replace($matches[0], '', $freeSearch);
$freeSearch = str_replace(' ', ' ', $freeSearch);
}
/*
* Pattern for: "non roulant"
*/
if (false !== stripos($freeSearch, 'non roulant')) {
$criteria['broken'] = true;
$criteria['form']['broken'] = true;
$found['broken'] = true;
$freeSearch = str_replace('non roulant', '', $freeSearch);
$freeSearch = str_replace(' ', ' ', $freeSearch);
}
/*
* Pattern for: "critair1, critair2, ..."
*/
if (preg_match('/critair([0-5])/i', $freeSearch, $matches)) {
$criteria['critair'] = (int) $matches[1];
$criteria['form']['critair'] = (int) $matches[1];
$found['critair'] = true;
$freeSearch = str_replace($matches[0], '', $freeSearch);
$freeSearch = str_replace(' ', ' ', $freeSearch);
}
/*
* Pattern for location location in the north of france
* /rechercher/north-france
*/
if (preg_match('/north france/i', $freeSearch, $matches)) {
$arr = array_column($this->vehicleRepository->findDistinctCitiesByZipCode(['59', '62', '80', '02', '08', '51', '76', '60', '27', '95', '78', '91', '77', '93', '94', '92', '75']), 'cityLocation');
$criteria['cityLocation'] = $arr;
$criteria['form']['cityLocation'] = implode(',', $arr);
$found['cityLocation'] = true;
$freeSearch = str_replace($matches[0], '', $freeSearch);
$freeSearch = str_replace(' ', ' ', $freeSearch);
}
$terms = array_filter(explode(' ', trim($freeSearch)));
foreach ($terms as $key => $term) {
$terms[$key] = str_replace('-', ' ', $term);
}
if ('' !== $freeSearch) {
foreach ($terms as $term) {
// Discard terms too short to be of importance
if (2 > strlen($term)) {
continue;
}
// Category
if (
!isset($found['category'])
&& (null !== $category = $this->vehicleRepository->findDistinctActiveCategoryLike(Urlizer::urlize($term)))
) {
$criteria['category'] = [$category['id']];
$criteria['form']['category'] = $category['id'];
$found['category'] = true;
continue;
}
// Maker
if (
!isset($found['maker'])
&& (null !== $maker = $this->vehicleRepository->findDistinctActiveMakerLike(Urlizer::urlize($term)))
) {
$criteria['makerSlug'] = [$maker['makerSlug']];
$criteria['form']['maker'] = Slugger::prettify($maker['makerSlug']);
$found['maker'] = true;
continue;
}
// Group
if (
[] !== $dbGroups = $this->vehicleRepository->findDistinctActiveGroupsLike(Urlizer::urlize($term))
) {
// Flatten the returned array of arrays and filter it for maker/model mismatch if any
$groups = array_filter(array_map(function ($group) use ($criteria) {
if (isset($criteria['makerSlug']) && !in_array($group['makerSlug'], $criteria['makerSlug'])) {
return null;
}
return $group['modelGroupSlug'];
}, $dbGroups));
if (isset($found['modelGroup'])) {
$criteria['modelGroupSlug'] = array_intersect($criteria['modelGroupSlug'], $groups);
} else {
$criteria['modelGroupSlug'] = $groups;
}
$criteria['form']['modelGroup'] = implode(',', array_map(fn ($group) => Slugger::prettify($group), $criteria['modelGroupSlug']));
if ([] === $criteria['modelGroupSlug']) {
$found['modelGroup'] = false;
continue;
}
$makers = array_unique(array_map(fn ($group) => $group['makerSlug'], $dbGroups));
if (isset($found['maker'])) {
$criteria['makerSlug'] = array_intersect($criteria['makerSlug'], $makers);
} else {
$criteria['makerSlug'] = $makers;
$found['maker'] = true;
}
$criteria['form']['maker'] = implode(',', array_map(fn ($maker) => Slugger::prettify($maker), $criteria['makerSlug']));
$found['modelGroup'] = true;
continue;
}
// Segmenttransmission
if (
!isset($found['segmentTransmission'])
&& null !== $transmission = $this->transmissionRepository->findOneByName(ucfirst(strtolower($term)))
) {
$criteria['segmentTransmission'][] = $transmission->getId();
$criteria['form']['segmentTransmission'][] = $transmission->getId();
$found['segmentTransmission'] = true;
continue;
}
// Segmenttaille
if (
!isset($found['segmentSize'])
&& [] !== $dbSegmenttaille = $this->vehicleRepository->findDistinctActiveSegmenttailleLike(Urlizer::urlize($term))
) {
$segmenttailles = array_map(fn ($segmenttaille) => $segmenttaille['segmentSizeSlug'], $dbSegmenttaille);
$criteria['segmentSizeSlug'] = $segmenttailles;
$criteria['form']['segmentSize'] = implode(',', array_map(fn ($segmenttaille) => Slugger::prettify($segmenttaille), $segmenttailles));
$found['segmentSize'] = true;
continue;
}
// Energy - there can be several, we don't need to stop at one
if (
null !== $energy = $this->energyRepository->findOneByName(ucfirst(strtolower($term)))
) {
$criteria['energy'][] = $energy->getCode();
$criteria['form']['energy'][] = $energy->getName();
$found['energy'] = true;
}
// Options - there can be several, we don't need to stop at one
if (
null !== $option = $this->optionRepository->findOneByNameLike($term)
) {
$criteria['options'][] = $option->getCodeSlug();
$criteria['form']['options'][] = Slugger::prettify($option->getCodeSlug());
$found['options'] = true;
}
}
if (isset($found['energy'])) {
$criteria['form']['energy'] = implode(', ', $criteria['form']['energy']);
}
if (isset($found['options'])) {
$criteria['form']['options'] = implode(', ', $criteria['form']['options']);
}
}
if ([] === $found) {
$criteria['sale'] = -1;
}
return $criteria;
}
private function findVehicle(Request $request, $id)
{
$vehicle = $this->vehicleRepository->find($id);
if (null === $vehicle
|| Vehicle::SALING_STATE_EXCLUS === $vehicle->getSalingState()
|| Vehicle::SALING_STATE_NO_STARTING_PRICE === $vehicle->getSalingState()
|| Vehicle::SALING_STATE_SUPPRIME === $vehicle->getSalingState()
) {
$request->getSession()->getFlashBag()->add('error', 'frontend.vehicle.view.notfound.flash.error');
return null;
}
return $vehicle;
}
/**
* If length of $text is higher than or equal to $nb, truncates it and add "...".
*
* @param string $text
* @param int $nb
*
* @return string
*/
private function truncateText($text, $nb)
{
return strlen($text) <= $nb ? $text : trim(substr($text, 0, $nb)).'..';
}
// Clear all search form parameters in session
private function clearSearchFormSession(SessionInterface $session, bool $removeSaleAndEvent = true): void
{
if (true === $removeSaleAndEvent) {
$session->remove('sale');
$session->remove('event');
}
$session->remove('form_values');
$session->remove('badges');
}
}