<?php
namespace App\Controller\Page;
use App\Component\Configuration\Util\Config;
use App\Decorator\ApplicantDecorator as ApplicantFormDecorator;
use App\Entity\Applicant;
use App\Entity\ApplicantForm;
use App\Entity\ApplyWithWhatsapp;
use App\Entity\Recruiter;
use App\Entity\SiteUserData;
use App\Entity\Vacancy;
use App\Event\ApplyWithWhatsappEvent;
use App\Event\GetResponseVacancyEvent;
use App\Event\NewsletterEvent;
use App\Event\VacancyEvents;
use App\Event\VacancyRedirectEvent;
use App\EventListener\FeatureFlagListener;
use App\Form\ApplyWithWhatsappType;
use App\Form\Setting\VacancySettingType;
use App\Model\Tealium\Base;
use App\Renderer\Page as PageRenderer;
use App\Renderer\VacancyRenderer;
use App\Service\ApplicantService;
use App\Service\VacancyService;
use App\Templating\Decorator as ThemeTemplateDecorator;
use App\Transformer\SiteUserDataToApplicantTransformer;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\NoResultException;
use Flagception\Manager\FeatureManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
#[Route(path: '/solliciteren')]
class ApplicantController extends AbstractController
{
private ApplicantFormDecorator $applicantFormDecorator;
private PageRenderer $pageRenderer;
private ThemeTemplateDecorator $themeTemplateDecorator;
private EventDispatcherInterface $eventDispatcher;
private ?Request $masterRequest;
private FeatureManagerInterface $featureManager;
private VacancyService $vacancyService;
private ApplicantService $applicantService;
private SessionInterface $session;
private Config $config;
public function __construct(
ApplicantFormDecorator $applicantFormDecorator,
PageRenderer $pageRenderer,
ThemeTemplateDecorator $themeTemplateDecorator,
EventDispatcherInterface $eventDispatcher,
RequestStack $requestStack,
FeatureManagerInterface $featureManager,
VacancyService $vacancyService,
ApplicantService $applicantService,
SessionInterface $session,
Config $config,
private readonly ParameterBagInterface $parameterBag,
private readonly SiteUserDataToApplicantTransformer $siteUserDataTransformer,
) {
$this->applicantFormDecorator = $applicantFormDecorator;
$this->pageRenderer = $pageRenderer;
$this->themeTemplateDecorator = $themeTemplateDecorator;
$this->eventDispatcher = $eventDispatcher;
$this->masterRequest = $requestStack->getMainRequest();
$this->featureManager = $featureManager;
$this->vacancyService = $vacancyService;
$this->applicantService = $applicantService;
$this->session = $session;
$this->config = $config;
}
/**
* @throws \Throwable
*/
#[Route(path: '/{id}', name: 'applicant_apply', requirements: ['id' => '\d+'])]
public function formAction(int $id, Request $request): RedirectResponse|Response
{
if ($request->query->has('async') && $request->query->get('async')) {
return $this->asyncFormAction($request, $id);
}
$vacancy = $this->getDoctrine()->getRepository(Vacancy::class)->find($id, null, null, true);
if (!$vacancy || !$vacancy->canApply()) {
$this->addFlash('warning', 'De vacature kan niet worden gevonden');
return $this->redirectToRoute('vacancies');
}
$event = new GetResponseVacancyEvent($vacancy, $request);
$this->eventDispatcher->dispatch($event, VacancyEvents::VACANCY_APPLICATION_INITIALIZE);
if (null !== $event->getResponse()) {
return $event->getResponse();
}
$applicant = new Applicant();
$params = ['id' => $id];
if ($request->query) {
$params = array_merge($request->query->all(), $params);
}
$form = $this->applicantFormDecorator->getForm(
$applicant,
$vacancy,
$this->generateUrl('vacancy_apply', $params)
);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$applicant->setLocale($request->getLocale());
$this->applicantFormDecorator->handleForm($form, $applicant, $vacancy);
$event = new GetResponseVacancyEvent($vacancy, $request);
$this->eventDispatcher->dispatch($event, VacancyEvents::VACANCY_APPLICATION_COMPLETED);
return $event->getResponse() ?? $this->applicantService->generateSuccessRedirect(
$applicant,
$applicant->getVacancy()
);
}
if (!$applicantForm = $vacancy->getApplicantForm()) {
$applicantForm = $this->getDoctrine()->getRepository(ApplicantForm::class)
->findOneBy(['default' => true]);
}
$response = $this
->pageRenderer
->renderPage(
'{vacancy_apply}',
$this->themeTemplateDecorator->getTemplate('@default/pages/vacancy_apply.html.twig'),
[
'applicantForm' => $applicantForm,
'applicant' => $applicant,
'vacancy' => $vacancy,
'form' => $form->createView(),
]
);
$response->setSharedMaxAge(0);
return $response;
}
/**
* @throws NoResultException
* @throws NonUniqueResultException
* @throws \JsonException
*/
#[Route(path: '/{id}/modal', name: 'applicant_apply_modal')]
public function formModalAction(int $id, Request $request, string $template = '@default/pages/Modal/vacancy_apply_modal.html.twig', ?string $action = null, ?string $errorRedirectUrl = null): JsonResponse|Response
{
if ($this->featureManager->isActive(FeatureFlagListener::FEATURE_CHILD_SITE)) {
$vacancyRepository = $this->getDoctrine()->getManager('parent')->getRepository(Vacancy::class);
$siteUserRepository = $this->getDoctrine()->getManager('parent')->getRepository(SiteUserData::class);
} else {
$vacancyRepository = $this->getDoctrine()->getRepository(Vacancy::class);
$siteUserRepository = $this->getDoctrine()->getRepository(SiteUserData::class);
}
$vacancy = $vacancyRepository->find($id);
$validStrategies = [VacancySettingType::DELETED_STRATEGY_SHOW_WITH_APPLY, VacancySettingType::DELETED_STRATEGY_SHOW_WITHOUT_APPLY];
if (!$vacancy &&
\in_array($this->config->get('site_vacancy_deleted_vacancy_strategy'), $validStrategies, true)
) {
$vacancy = $vacancyRepository->findDeletedVacancy($id);
}
if (!$vacancy) {
$this->addFlash('warning', 'De vacature kan niet worden gevonden');
return new Response('');
}
$event = new GetResponseVacancyEvent($vacancy, $request);
$this->eventDispatcher->dispatch($event, VacancyEvents::VACANCY_APPLICATION_INITIALIZE);
if (!$request->isXmlHttpRequest() && null !== $event->getResponse()) {
return $event->getResponse();
}
$sessionName = sprintf('vacancy_%d_application', $id);
$applicant = new Applicant();
$applicant->setVacancy($vacancy);
if ($this->session->has($sessionName)) {
$applicant = $this->session->get($sessionName)['applicant'];
}
if ($siteUserData = $siteUserRepository->findOneBy([
'user' => $this->getUser(),
])) {
$applicant = $this->siteUserDataTransformer->transform($siteUserData);
$applicant->setEmail($this->getUser()?->getEmail());
}
$form = $this->applicantFormDecorator->getForm(
$applicant,
$vacancy,
$action ?? $this->generateUrl('applicant_apply_modal', array_merge(['id' => $id]))
);
if ($this->session->has($sessionName)) {
foreach ($this->session->get($sessionName)['errors'] as $typeName => $error) {
if (\is_array($error)) {
$error = reset($error);
}
if ($form->has($typeName)) {
$form->get($typeName)->addError(new FormError($error));
} else {
$form->addError(new FormError($error));
}
}
$this->session->remove($sessionName);
}
$form->handleRequest($request);
if (!$applicantForm = $vacancy->getApplicantForm()) {
$applicantForm = $this->getDoctrine()->getRepository(ApplicantForm::class)
->findOneBy(['default' => true]);
}
if ($form->isSubmitted()) {
if ($form->isValid()) {
$applicant->setLocale($request->getLocale());
$this->processApplicant($form, $applicant, $vacancy);
$event = new GetResponseVacancyEvent($vacancy, $request);
$this->eventDispatcher->dispatch($event, VacancyEvents::VACANCY_APPLICATION_COMPLETED);
if (!$request->isXmlHttpRequest() && null !== $event->getResponse()) {
return $event->getResponse();
}
if ($request->isXmlHttpRequest()) {
return new JsonResponse([
'redirect' => $this->applicantService->generateSuccessRedirectUrl($applicant, $vacancy),
]);
}
return $this->applicantService->generateSuccessRedirect($applicant, $vacancy);
}
/** @var Applicant $applicant */
$applicant = $form->getData()
->setCVFile(null)
->setMotivationFile(null);
$this->session->set($sessionName, [
'applicant' => $applicant,
'errors' => $this->getErrorsFromForm($form),
]);
if (!$request->isXmlHttpRequest()) {
return $errorRedirectUrl ? $this->redirect($errorRedirectUrl) :
$this->redirectToRoute(
'vacancy_detail',
[
'id' => $vacancy->getId(),
'slug' => $vacancy->getSlug(),
'apply' => 'apply',
]
);
}
}
return $this->render(
$this->themeTemplateDecorator->getTemplate($template),
[
'applicantForm' => $applicantForm,
'applicant' => $applicant,
'vacancy' => $vacancy,
'form' => $form->createView(),
]
);
}
/**
* @throws NoResultException
* @throws NonUniqueResultException
* @throws \JsonException
*/
#[Route(path: '/{id}/inline', name: 'applicant_apply_inline', options: ['expose' => true], requirements: ['id' => '\d+'])]
public function inlineFormAction(int $id, Request $request): Response
{
return $this->formModalAction(
$id,
$request,
'@default/pages/vacancy_apply_inline.html.twig',
$this->generateUrl('applicant_apply_inline', ['id' => $id])
);
}
#[Route(path: '/open/inline', name: 'applicant_open_apply_inline', options: ['expose' => true])]
public function inlineOpenFormAction(): Response
{
$applicant = new Applicant();
$form = $this->applicantFormDecorator->getOpenForm($applicant, $this->generateUrl('applicant_open_apply_inline'));
if (!$form instanceof FormInterface) {
throw $this->createNotFoundException();
}
return $this->render('@default/pages/vacancy_apply_inline.html.twig', [
'form' => $form->createView(),
]);
}
#[Route(path: '/inline/send', name: 'applicant_apply_inline_post', options: ['expose' => true])]
public function inlineFormPostAction(Request $request): Response
{
$vacancy = null;
if ($request->get('id')) {
$vacancy = $this->getDoctrine()->getRepository(Vacancy::class)->find($request->get('id'));
}
$applicant = new Applicant();
$form = $this->applicantFormDecorator->getOpenForm($applicant);
if ($vacancy) {
$form = $this->applicantFormDecorator->getForm(
$applicant,
$vacancy
);
}
if (!$form) {
return new JsonResponse([]);
}
$form->handleRequest($request);
if ($form->isSubmitted()) {
if ($form->isValid()) {
$applicant->setLocale($request->getLocale());
$this->processApplicant($form, $applicant, $vacancy);
$applicantIdForDatalayer = $this->config->get('site_google_show_external_applicant_id') ?
$applicant->getExternalId() : $applicant->getId();
return new JsonResponse([
'success' => true,
'applicant_id' => $applicantIdForDatalayer,
'applicant_query' => $this->config->get('site_google_applicant_query_name'),
]);
}
return new JsonResponse([
'success' => false,
'error' => 'Form could not be submitted',
'validation' => $this->getErrorsFromForm($form),
], 500);
}
return new JsonResponse([]);
}
/**
* @throws \JsonException
*/
private function processApplicant(FormInterface $form, Applicant $applicant, ?Vacancy $vacancy)
{
if ($vacancy) {
$this->applicantFormDecorator->handleForm($form, $applicant, $vacancy);
} else {
$this->applicantFormDecorator->handleOpenForm($form, $applicant);
}
if ($this->parameterBag->get('site_newsletter_in_privacy_statement')) {
$this->eventDispatcher->dispatch(
new NewsletterEvent($applicant->getEmail()),
NewsletterEvent::REGISTERED
);
}
}
private function getErrorsFromForm(FormInterface $form): array
{
$errors = [];
foreach ($form->getErrors() as $error) {
$errors[] = $error->getMessage();
}
foreach ($form->all() as $childForm) {
if (($childForm instanceof FormInterface) && $childErrors = $this->getErrorsFromForm($childForm)) {
$errors[$childForm->getName()] = $childErrors;
}
}
return $errors;
}
/**
* @throws SyntaxError
* @throws RuntimeError
* @throws LoaderError
*/
public function asyncFormAction(Request $request, int $id): Response
{
try {
$applicantForm = $this->applicantService->getAsyncApplicantFormForVacancy($id);
$vacancyWrapper = $this->vacancyService->getVacancyFromRequest($request);
$vacancy = $vacancyWrapper?->vacancy;
} catch (\Exception) {
$this->addFlash('warning', 'Something went wrong');
return $this->redirectToRoute('vacancy_detail_without_slug', ['id' => $id]);
}
$applicant = new Applicant();
$applicant->setVacancy($vacancy);
$form = $this->applicantFormDecorator->createForm(
$applicantForm,
$applicant,
$this->generateUrl('applicant_apply_modal_async', ['id' => $id]),
$vacancy
)->getForm();
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
try {
$responseData = $this->applicantService->handleForm($request, $applicant, $vacancy);
return $this->redirectToRoute(
'vacancy_apply_thanks',
[
'id' => $vacancy->getId(),
$this->config->get('site_google_applicant_query_name') => $responseData['id'],
]
);
} catch (\Exception) {
$this->addFlash('warning', 'Something went wrong');
}
return $this->redirectToRoute('vacancy_detail_without_slug', ['id' => $id]);
}
return $this
->pageRenderer
->renderPage(
'{vacancy_apply}',
$this->themeTemplateDecorator->getTemplate('@default/pages/vacancy_apply.html.twig'),
[
'applicantForm' => $applicantForm,
'applicant' => $applicant,
'vacancy' => $vacancy,
'form' => $form->createView(),
]
)->setSharedMaxAge(0);
}
#[Route(path: '/{id}/async', name: 'applicant_apply_modal_async')]
public function asyncFormModalAction(Request $request, int $id): Response
{
try {
$applicantForm = $this->applicantService->getAsyncApplicantFormForVacancy($id);
$vacancyWrapper = $this->vacancyService->getVacancyFromRequest($request);
$vacancy = $vacancyWrapper?->vacancy;
} catch (\Exception) {
$this->addFlash('warning', 'Something went wrong');
return $this->redirectToRoute('vacancy_detail_without_slug', ['id' => $id]);
}
$applicant = new Applicant();
$applicant->setVacancy($vacancy);
$form = $this->applicantFormDecorator->createForm(
$applicantForm,
$applicant,
$this->generateUrl('applicant_apply_modal_async', ['id' => $id]),
$vacancy
)->getForm();
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
try {
$responseData = $this->applicantService->handleForm($request, $applicant, $vacancy);
return $this->redirectToRoute(
'vacancy_apply_thanks',
[
'id' => $vacancy->getId(),
$this->config->get('site_google_applicant_query_name') => $responseData['id'],
]
);
} catch (\Exception) {
$this->addFlash('warning', 'Something went wrong');
}
return $this->redirectToRoute('vacancy_detail_without_slug', ['id' => $id]);
}
return $this->render(
$this->themeTemplateDecorator->getTemplate('@default/pages/Modal/vacancy_apply_modal.html.twig'),
[
'applicantForm' => $applicantForm,
'applicant' => $applicant,
'vacancy' => $vacancy,
'form' => $form->createView(),
]
);
}
/**
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/
#[Route(path: '/thanks', name: 'vacancy_apply_thanks', options: ['expose' => true])]
public function thanksAction(
Request $request,
EntityManagerInterface $entityManager,
EntityManagerInterface $parentEntityManager,
VacancyRenderer $vacancyRenderer
): Response {
$applicantId = $request->get($this->config->get('site_google_applicant_query_name'));
if (null === $applicantId) {
throw $this->createNotFoundException();
}
$applicantRepo = $entityManager->getRepository(Applicant::class);
if ($this->featureManager->isActive(FeatureFlagListener::FEATURE_CHILD_SITE)) {
$applicantRepo = $parentEntityManager->getRepository(Applicant::class);
}
/** @var Applicant $applicant */
$applicant = $applicantRepo->find($applicantId);
$vacancyRepo = $entityManager->getRepository(Vacancy::class);
if ($this->featureManager->isActive(FeatureFlagListener::FEATURE_CHILD_SITE)) {
$vacancyRepo = $parentEntityManager->getRepository(Vacancy::class);
}
$vacancy = null;
if ($id = $request->get('id')) {
$vacancy = $vacancyRepo->find($id);
}
if (
!$vacancy &&
!empty($this->config->get('site_google_vacancy_query_name')) &&
$request->query->has($this->config->get('site_google_vacancy_query_name'))
) {
$externalId = $request->query->get($this->config->get('site_google_vacancy_query_name'));
$vacancy = $vacancyRepo->findOneBy(['externalId' => $externalId]);
if (!$vacancy) {
$vacancy = $vacancyRepo->findOneBy(['externalReference' => $externalId]);
}
}
$recruiter = null;
// Applicant can be null when redirecting from external application
if ($applicant) {
$recruiter = $this->applicantService->getApplicantOwner($applicant);
}
if (!$recruiter) {
$recruiter = $entityManager->getRepository(Recruiter::class)->findOneBy([]);
}
$tealium = null;
if ($vacancy && $this->featureManager->isActive(FeatureFlagListener::FEATURE_TEALIUM)) {
$tealium = Base::createForVacancy($vacancy, $this->config);
$tealium->addProperty('job_application_id', $applicant?->getId() ?? $applicantId);
}
$multimedia = null;
if ($this->featureManager->isActive(FeatureFlagListener::FEATURE_MULTI_MEDIA)) {
$multimedia = $this->applicantService->getSuccessMultiMedia($vacancy, $request->getLocale());
}
if ($vacancy instanceof Vacancy) {
$vacancyRenderer->calculateGallery($vacancy);
}
return $this->pageRenderer
->renderPage(
'{vacancy_apply_thanks}',
'@default/pages/vacancy_apply_thanks.html.twig',
[
'vacancy' => $vacancy,
'recruiter' => $recruiter,
'applicant' => $applicant,
'multimedia' => $multimedia,
'e_commerce_script' => $this->applicantService->renderECommerceScript($request, $vacancy),
'head_end_script' => $this->applicantService->renderHeadEndScriptForVacancyDetail($vacancy),
'body_end_script' => $this->applicantService->renderBodyEndScriptForVacancyDetail(),
],
[],
[],
null,
null,
null,
[
'tealium_element' => $tealium,
]
);
}
/**
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/
#[Route(path: '/', name: 'applicant_apply_open')]
public function applyOpenAction(Request $request): Response
{
$applicant = new Applicant();
$form = $this->applicantFormDecorator->getOpenForm($applicant, $this->generateUrl('applicant_apply_open'));
if (!$form instanceof FormInterface) {
throw $this->createNotFoundException();
}
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$applicant->setLocale($request->getLocale());
$this->applicantFormDecorator->handleOpenForm($form, $applicant);
$this->applicantService->generateSuccessRedirect(
$applicant
);
}
return $this->pageRenderer
->renderPage(
'{vacancy_form}',
$this->themeTemplateDecorator->getTemplate('@default/pages/vacancy_apply_open.html.twig'),
[
'applicant' => $applicant,
'form' => $form->createView(),
]
);
}
/**
* @throws \Exception
*/
public function connexysFormModalAction(int $id, Request $request, EntityManagerInterface $entityManager): Response
{
$vacancy = $entityManager->getRepository(Vacancy::class)->find($id);
return $this->render($this->themeTemplateDecorator->getTemplate('@default/pages/Modal/connexys_vacancy_apply_modal.html.twig'), [
'vacancy' => $vacancy,
]);
}
/**
* @throws \Exception
*/
#[Route(path: '/form/connexys-open', name: 'applicant_connexys_open_modal_snippet')]
public function connexysOpenFormModalAction(): Response
{
return $this->render($this->themeTemplateDecorator->getTemplate('pages/Modal/connexys_vacancy_open_apply_modal.html.twig'));
}
/**
* Middleware for redirects on external url.
*/
#[Route(path: '/redirect/{id}', name: 'redirect_to_apply')]
public function externalUrlRedirectAction(Vacancy $vacancy): RedirectResponse
{
if (empty($vacancy->getExternalUrl())) {
return $this->redirectToRoute('vacancy_detail', ['id' => $vacancy->getId(), 'slug' => $vacancy->getSlug()]);
}
$this->eventDispatcher->dispatch(new VacancyRedirectEvent($vacancy));
return $this->redirect($vacancy->getExternalUrl());
}
public function connexysPageFormModalAction(EntityManagerInterface $entityManager): Response
{
$route = $entityManager->getRepository(RouteEntity::class)->findOneBy([
'name' => $this->masterRequest->get('_route'),
]);
// check if route exists and page exists
/** @var Page $page */
if (!$route || !$page = $route->getPage()) {
return new Response('');
}
// check if connexys page configuration exists
if (!$connexysConfiguration = $page->getConnexysPageConfiguration()) {
return new Response('');
}
// check if all mandatory attributes are not empty
if (
empty($connexysConfiguration->getVacancyTitle())
) {
return new Response('');
}
return $this->render($this->themeTemplateDecorator->getTemplate('@default/pages/Modal/connexys_page_vacancy_apply_modal.html.twig'), [
'originalVacancyId' => $connexysConfiguration->getOriginalVacancyId(),
'vacancyId' => $connexysConfiguration->getVacancyId(),
'vacancyTitle' => $connexysConfiguration->getVacancyTitle(),
]);
}
#[Route(path: '/whatsapp_form', name: 'whatsapp_form', options: ['expose' => true])]
public function whatsappForm(Request $request): Response
{
$applyWithWhatsapp = new ApplyWithWhatsapp();
$form = $this->createForm(ApplyWithWhatsappType::class, $applyWithWhatsapp);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($applyWithWhatsapp);
$em->flush();
$this->eventDispatcher->dispatch(new ApplyWithWhatsappEvent($applyWithWhatsapp), ApplyWithWhatsappEvent::APPLIED_WITH_WHATSAPP);
return new Response($this->parameterBag->get('site_apply_with_whatsapp_thanks'));
}
return $this->render('form/apply_with_whatsapp.html.twig', [
'form' => $form->createView(),
]);
}
#[Route(path: '/open-application-embed', name: 'open_application_embed', options: ['expose' => true])]
public function openApplicationEmbed(): Response
{
return $this->render('api/open_application_embed.html.twig');
}
}