<?php
namespace App\Decorator;
use App\Component\Captcha\Form\CaptchaType;
use App\Component\Configuration\Util\Config;
use App\Entity\Applicant;
use App\Entity\ApplicantForm;
use App\Entity\Company;
use App\Entity\GdprStatement;
use App\Entity\KnockoutQuestion;
use App\Entity\PhoneNumber;
use App\Entity\Repository\ApplicantFormRepository;
use App\Entity\Status;
use App\Entity\Vacancy;
use App\Entity\VacancyQuestion;
use App\Event\ApplicantEvent;
use App\EventListener\FeatureFlagListener;
use App\Form\CheckboxHtmlType;
use App\Util\StringToEntityUtil;
use Cocur\Slugify\Slugify;
use Doctrine\ORM\EntityManagerInterface;
use Flagception\Manager\FeatureManagerInterface;
use League\Flysystem\Adapter\Local;
use MobileDetectBundle\DeviceDetector\MobileDetector;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Exception\InvalidArgumentException;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Contracts\Translation\TranslatorInterface;
use Vich\UploaderBundle\Metadata\MetadataReader;
class ApplicantDecorator extends FormDecorator
{
private Local $adapter;
private EntityManagerInterface $parentEntityManager;
private ApplicantFormRetriever $applicantFormRetriever;
public function __construct(
EntityManagerInterface $manager,
FormFactoryInterface $formFactory,
EventDispatcherInterface $eventDispatcher,
ParameterBagInterface $parameterBag,
StringToEntityUtil $stringToEntityUtil,
MobileDetector $mobileDetect,
FeatureManagerInterface $featureManager,
Local $applicantUploadAdapter,
$parentEntityManager,
Config $config,
protected MetadataReader $vichMetadataReader,
ApplicantFormRetriever $applicantFormRetriever,
TranslatorInterface $translator,
) {
parent::__construct(
$manager,
$formFactory,
$eventDispatcher,
$parameterBag,
$stringToEntityUtil,
$mobileDetect,
$featureManager,
$config,
$vichMetadataReader,
$translator
);
$this->adapter = $applicantUploadAdapter;
$this->parentEntityManager = $parentEntityManager;
$this->applicantFormRetriever = $applicantFormRetriever;
}
/** @throws \JsonException */
public function getForm(Applicant $applicant, Vacancy $vacancy, ?string $action = null): FormInterface
{
return $this->createForm(
$this->applicantFormRetriever->retrieve($vacancy),
$applicant,
$action,
$vacancy
)->getForm();
}
/**
* @throws \JsonException
*/
public function handleOpenForm(FormInterface $form, Applicant $applicant): bool
{
$applicantForm = $applicant->getApplicantForm()
?? $this->manager->getRepository(ApplicantForm::class)->findOneBy(['openForm' => true]);
if (!$applicantForm instanceof ApplicantForm) {
return false;
}
$applicantForm = json_decode($applicantForm->getFields(), true, 512, \JSON_THROW_ON_ERROR);
$data = [];
foreach ($applicantForm as $item) {
if ($value = $form[$item['name']]->getData()) {
if ($value instanceof Company) {
$value = $value->getId();
}
$data[$item['name']] = $value;
}
}
$applicant->setData(json_encode($data, \JSON_THROW_ON_ERROR));
$applicant->setApplicantSendToApplicantMail(false);
if (!$this->featureManager->isActive(FeatureFlagListener::FEATURE_CHILD_SITE) &&
$defaultStatusObject = $this->config->get('site_applicant_default_status')) {
/** @var Status $defaultStatus */
$defaultStatus = $this->manager->getRepository(Status::class)->find($defaultStatusObject->getId());
if ($defaultStatus) {
$applicant->setStatus($defaultStatus);
}
}
$this->eventDispatcher->dispatch(new ApplicantEvent($applicant, $form), ApplicantEvent::EVENT_PRE_PERSIST);
$this->manager->persist($applicant);
$this->manager->flush();
$this->eventDispatcher->dispatch(new ApplicantEvent($applicant, $form), ApplicantEvent::EVENT_POST_PERSIST);
return true;
}
public function getOpenForm(Applicant $applicant, ?string $action = null, ?string $name = null, ?string $locale = null): ?FormInterface
{
$applicantForm = $applicant->getApplicantForm() ?? $this->fetchOpenApplicationForm();
if (!$applicantForm instanceof ApplicantForm) {
return null;
}
try {
$form = $this->createForm(
$applicantForm,
$applicant,
$action,
null,
$name,
$locale
);
$applicant->setApplicantForm($applicantForm);
$applicantForm->addApplicant($applicant);
return $form->getForm();
} catch (InvalidArgumentException|\JsonException) {
// Name is most likely invalid due to xss violations, should return absolutely nothing.
return null;
}
}
/**
* @throws \JsonException
*/
public function createForm(
ApplicantForm $applicantForm,
Applicant $applicant,
?string $action,
?Vacancy $vacancy,
?string $name = null,
?string $locale = null
): FormBuilderInterface {
if (!$name) {
$name = $vacancy ? 'form' : 'open';
}
$form = $this->formFactory->createNamedBuilder(
$name,
FormType::class,
$applicant,
[
'action' => $action,
'attr' => [
'class' => 'recaptcha-form',
'data-component' => 'Form',
'data-form-type' => $applicantForm->isOpenForm() ? 'open-apply-form' : 'apply-form',
],
]
);
if ($vacancy instanceof Vacancy
&& $this->featureManager->isActive(FeatureFlagListener::FEATURE_VACANCY_KNOCKOUT_QUESTIONS)
&& !empty($vacancy->getKnockoutQuestions())
) {
foreach ($vacancy->getKnockoutQuestions() as $knockoutQuestion) {
$form->add(sprintf('knockoutQuestion_%s', $knockoutQuestion->getId()), ChoiceType::class, [
'label' => $knockoutQuestion,
'expanded' => true,
'mapped' => false,
'choices' => [
'Yes' => KnockoutQuestion::ANSWER_YES,
'No' => KnockoutQuestion::ANSWER_NO,
],
'choice_attr' => [
'required' => true,
],
'data' => KnockoutQuestion::ANSWER_YES,
]);
}
}
$form = $this->decorateFields($form, json_decode($applicantForm->getFields(), true, 512, \JSON_THROW_ON_ERROR), $applicant, $locale);
if ($this->config->get('site_applicant_apply_via_linkedin')) {
$form->add('linkedinProfileUrl');
}
if ($vacancy instanceof Vacancy && !empty($vacancy->getVacancyQuestions())) {
$form = $this->processVacancyQuestions($form, $vacancy);
}
if ($applicantForm->hasPrivacyStatement()) {
$form->add('acceptedPrivacyPolicy', CheckboxHtmlType::class, [
'label' => $applicantForm->getPrivacyStatementText(),
'data' => false,
'required' => $applicantForm->isPrivacyStatementIsRequired(),
]);
}
if (
$this->featureManager->isActive(FeatureFlagListener::FEATURE_GDPR) &&
!$applicantForm->getGdprStatements()->isEmpty()
) {
$defaultValue = $this->config->get('applicant_fallback_gdpr');
if ($defaultValue) {
$defaultValue = $this->manager->getRepository(GdprStatement::class)->find($defaultValue->getId());
}
if (1 === $applicantForm->getGdprStatements()->count()) {
$form->add(
'gdpr',
CheckboxHtmlType::class,
[
'label' => $applicantForm->getGdprStatements()->first()->getStatement(),
'required' => $this->config->get('applicant_gdpr_required'),
]
);
$form->get('gdpr')->addModelTransformer(new CallbackTransformer(
function ($value) {
return !empty($value);
},
function ($value) use ($applicantForm, $defaultValue) {
if ($value) {
return $applicantForm->getGdprStatements()->first();
}
return $defaultValue;
}
));
} else {
$firstInCollection = !$applicantForm->getGdprStatements()->isEmpty() ?
$applicantForm->getGdprStatements()->first()->getId() : null;
$defaultValue = $defaultValue ?? $firstInCollection;
$form->add('gdpr', ChoiceType::class, [
'label' => false,
'expanded' => true,
'multiple' => false,
'choices' => array_column($applicantForm->getGdprStatements()->toArray(), 'id', 'statement'),
'required' => $this->config->get('applicant_gdpr_required'),
'data' => $defaultValue?->getId(),
'placeholder' => false,
]);
$form->get('gdpr')->addModelTransformer(new CallbackTransformer(
function ($value) {
return $value;
},
function ($value) use ($applicantForm, $defaultValue) {
if (!$value) {
return $defaultValue;
}
$gdprStatements = $applicantForm->getGdprStatements();
$gdprStatements->filter(function (GdprStatement $gdprStatement) use ($value) {
return $value === $gdprStatement->getId();
});
return $gdprStatements->first();
}
));
}
}
if ($applicantForm->hasCaptcha()) {
$form->add('captcha', CaptchaType::class);
}
return $form;
}
public function processVacancyQuestions(FormBuilderInterface $form, Vacancy $vacancy): FormBuilderInterface
{
foreach ($vacancy->getVacancyQuestions() as $question) {
switch ($question->getType()) {
case 1:
case 2:
case 3:
$choices = [];
foreach ($question->getAnswers() as $answer) {
$choices[$answer->getValue()] = $answer->getExternalReference();
}
$form->add(sprintf('vacancyQuestion%s', $question->getExternalReference()), ChoiceType::class, [
'multiple' => 3 === $question->getType(),
'expanded' => \in_array($question->getType(), [2, 3], true),
'choices' => $choices,
'mapped' => false,
'label' => $question->getValue(),
]);
break;
case 4:
$form->add(sprintf('vacancyQuestion%s', $question->getExternalReference()), TextType::class, [
'mapped' => false,
'required' => true,
'label' => $question->getValue(),
]);
break;
}
}
return $form;
}
/**
* @throws \JsonException
*/
public function handleForm(FormInterface $form, Applicant $applicant, Vacancy $vacancy): bool
{
$applicantForm = $this->applicantFormRetriever->retrieve($vacancy);
$data = [];
$questionAnswers = [];
foreach (json_decode($applicantForm->getFields(), true, 512, \JSON_THROW_ON_ERROR) as $item) {
if ($value = $form[$item['name']]->getData()) {
if ($value instanceof UploadedFile && !$form[$item['name']]->getConfig()->getMapped()) {
$originalFilename = pathinfo($value->getClientOriginalName(), \PATHINFO_FILENAME);
// this is needed to safely include the file name as part of the URL
$safeFilename = (new Slugify())->slugify($originalFilename);
$newFilename = sprintf('%s-%s.%s', $safeFilename, uniqid('', true), $value->guessExtension());
try {
$value->move(
$this->adapter->getPathPrefix(),
$newFilename
);
} catch (FileException) {
}
$value = '__FILE__'.$newFilename;
}
if ($value instanceof PhoneNumber && !method_exists($applicant, 'set'.$item['name'])) {
$value = serialize($value);
}
$data[$item['name']] = $value;
}
}
if (!empty($vacancy->getVacancyQuestions())) {
foreach ($vacancy->getVacancyQuestions() as $question) {
/** @var VacancyQuestion $question */
if ($value = $form[sprintf('vacancyQuestion%s', $question->getExternalReference())]->getData()) {
$questionAnswers[$question->getExternalReference()] = $value;
}
}
}
$applicant->setData(json_encode($data, \JSON_THROW_ON_ERROR));
$applicant->setQuestionAnswerData(json_encode($questionAnswers, \JSON_THROW_ON_ERROR));
$applicant->setVacancy($vacancy);
$applicant->setApplicantSendToApplicantMail(false);
if (!$this->featureManager->isActive(FeatureFlagListener::FEATURE_CHILD_SITE) &&
$defaultStatusObject = $this->config->get('site_applicant_default_status')) {
/** @var Status $defaultStatus */
$defaultStatus = $this->manager->getRepository(Status::class)->find($defaultStatusObject->getId());
if ($defaultStatus) {
$applicant->setStatus($defaultStatus);
}
}
$this->eventDispatcher->dispatch(
new ApplicantEvent($applicant, $form, false),
ApplicantEvent::EVENT_PRE_PERSIST
);
if ($this->featureManager->isActive(FeatureFlagListener::FEATURE_APPLICANT_DONT_SAVE_TO_DB)) {
$this->eventDispatcher->dispatch(
new ApplicantEvent($applicant, $form),
ApplicantEvent::EVENT_POST_PERSIST
);
return true;
}
$fileName = $applicant->getFileName();
$fileSize = $applicant->getFileSize();
$fileNameMotivation = $applicant->getFileNameMotivation();
$fileSizeMotivation = $applicant->getFileSizeMotivation();
if ($this->featureManager->isActive(FeatureFlagListener::FEATURE_VACANCY_KNOCKOUT_QUESTIONS)
&& !empty($vacancy->getKnockoutQuestions())
) {
$answerData = [];
$rejectApplicant = false;
foreach ($vacancy->getKnockoutQuestions() as $knockoutQuestion) {
$questionAnswer = $form->get(sprintf('knockoutQuestion_%s', $knockoutQuestion->getId()))->getData();
$answerData[$knockoutQuestion->getId()] = $questionAnswer;
if (KnockoutQuestion::ANSWER_NO === $questionAnswer) {
$rejectApplicant = true;
}
}
$applicant
->setRejectedByKnockout($rejectApplicant)
->setKnockoutQuestionAnswers($answerData);
}
if ($this->featureManager->isActive(FeatureFlagListener::FEATURE_CHILD_SITE)) {
$this->parentEntityManager->persist($applicant);
$this->parentEntityManager->flush();
} else {
$this->manager->persist($applicant);
$this->manager->flush();
}
if ($fileName && $fileSize) {
$applicant->setFileName($fileName);
$applicant->setFileSize($fileSize);
}
if ($fileNameMotivation && $fileSizeMotivation) {
$applicant->setFileNameMotivation($fileNameMotivation);
$applicant->setFileSizeMotivation($fileSizeMotivation);
}
if ($this->featureManager->isActive(FeatureFlagListener::FEATURE_CHILD_SITE)) {
$this->parentEntityManager->persist($applicant);
$this->parentEntityManager->flush();
} else {
$this->manager->persist($applicant);
$this->manager->flush();
}
$this->eventDispatcher->dispatch(
new ApplicantEvent($applicant, $form),
ApplicantEvent::EVENT_POST_PERSIST
);
return true;
}
public function fetchOpenApplicationForm(int $applicantFormId = 0): ?ApplicantForm
{
/** @var ApplicantFormRepository $applicantFormRepository */
$applicantFormRepository = $this->manager->getRepository(ApplicantForm::class);
if (0 !== $applicantFormId
&& ($applicantForm = $applicantFormRepository->find($applicantFormId)) instanceof ApplicantForm
) {
return $applicantForm;
}
$applicantForm = $this->config->get('site_vacancy_mobile_open_application_form');
if ($this->mobileDetect->isMobile() && $applicantForm instanceof ApplicantForm) {
return $applicantForm;
}
return $applicantFormRepository->findOneBy(['openForm' => true]);
}
}