<?php
namespace App\Service;
use App\Client\MultiSiteClient;
use App\Component\Configuration\Util\Config;
use App\Entity\Applicant;
use App\Entity\ApplicantForm;
use App\Entity\ApplicantStatus;
use App\Entity\Company;
use App\Entity\GdprStatement;
use App\Entity\MultiMedia;
use App\Entity\OptionValue;
use App\Entity\Recruiter;
use App\Entity\Status;
use App\Entity\Vacancy;
use App\Event\ApplicantEvent;
use App\EventListener\FeatureFlagListener;
use App\Form\ApplicantFilterType;
use App\Form\Setting\ApplicantSettingType;
use App\Manager\MultiMediaManager;
use App\Type\DataTable\ApplicantTableType;
use App\Type\DataTable\ORMApplicantTableType;
use App\Util\AdminFilterUtil;
use App\Util\PhoneNumberUtil;
use App\Util\StringToEntityUtil;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\EntityManagerInterface;
use Flagception\Manager\FeatureManagerInterface;
use JMS\Serializer\SerializerInterface;
use Omines\DataTablesBundle\DataTable;
use Omines\DataTablesBundle\DataTableFactory;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
class ApplicantService
{
public const OPEN_APPLICATION_ID = 0;
private const TYPE_FILE = 'file';
private const TYPE_PHONE = 'phone';
private ?Request $request;
private EntityManagerInterface $entityManager;
private ValidatorInterface $validator;
private StringToEntityUtil $stringToEntityUtil;
private EventDispatcherInterface $eventDispatcher;
private MultiSiteClient $client;
private SerializerInterface $serializer;
private RouterInterface $router;
private Environment $twig;
private FeatureManagerInterface $featureManager;
private Config $config;
private AdminFilterUtil $adminFilterUtil;
private DataTableFactory $dataTableFactory;
private AuthorizationCheckerInterface $authorizationChecker;
private MultiMediaManager $multiMediaManager;
/**
* ApplicantService constructor.
*/
public function __construct(
RequestStack $requestStack,
EntityManagerInterface $entityManager,
ValidatorInterface $validator,
StringToEntityUtil $stringToEntityUtil,
EventDispatcherInterface $eventDispatcher,
MultiSiteClient $multiSiteClient,
SerializerInterface $serializer,
RouterInterface $router,
Config $config,
Environment $twig,
FeatureManagerInterface $featureManager,
AdminFilterUtil $adminFilterUtil,
DataTableFactory $dataTableFactory,
AuthorizationCheckerInterface $authorizationChecker,
MultiMediaManager $multiMediaManager
) {
$this->request = $requestStack->getCurrentRequest();
$this->entityManager = $entityManager;
$this->validator = $validator;
$this->stringToEntityUtil = $stringToEntityUtil;
$this->eventDispatcher = $eventDispatcher;
$this->client = $multiSiteClient;
$this->serializer = $serializer;
$this->router = $router;
$this->config = $config;
$this->twig = $twig;
$this->featureManager = $featureManager;
$this->adminFilterUtil = $adminFilterUtil;
$this->dataTableFactory = $dataTableFactory;
$this->authorizationChecker = $authorizationChecker;
$this->multiMediaManager = $multiMediaManager;
}
/**
* @param string[] $contents
* @param UploadedFile[] $files
*
* @throws \RuntimeException
* @throws \JsonException
*/
public function createApplicant(array $contents, array $files): ?Applicant
{
$applicant = new Applicant();
$vacancyId = (int) $this->request->get('vacancy');
$applicantFormRepository = $this->entityManager->getRepository(ApplicantForm::class);
if (self::OPEN_APPLICATION_ID !== $vacancyId) {
/** @var Vacancy $vacancy */
$vacancy = $this->entityManager->getRepository(Vacancy::class)->find($vacancyId);
$applicant->setVacancy($vacancy);
$applicantForm = $vacancy->getApplicantForm();
} else {
$applicantForm = $applicantFormRepository->findOneBy(['openForm' => true]);
}
if (!$applicantForm) {
$applicantForm = $applicantFormRepository->findOneBy(['default' => true]);
}
$formFields = json_decode($applicantForm->getFields(), true, 512, \JSON_THROW_ON_ERROR);
foreach ($formFields as $formField) {
$field = $formField['name'];
if (self::TYPE_PHONE === $formField['type']) {
$contents[$field] = PhoneNumberUtil::createPhoneNumber($contents[$field]);
}
if (isset($formField['required']) && $formField['required']) {
if (self::TYPE_FILE !== $formField['type'] && !isset($contents[$field])) {
throw new \RuntimeException(sprintf("Mandatory field '%s' is missing", $field));
}
if (self::TYPE_FILE === $formField['type'] && !\array_key_exists($field, $files)) {
throw new \RuntimeException(sprintf("Mandatory file '%s' is missing", $field));
}
}
if (!method_exists($applicant, 'set'.$field)) {
continue;
}
if (self::TYPE_FILE !== $formField['type']) {
$applicant->{'set'.$field}($contents[$field] ?? null);
}
if (self::TYPE_FILE === $formField['type']) {
$applicant->{'set'.$field}($files[$field] ?? null);
}
}
if (!empty($contents['utm']) && \is_array($contents['utm'])) {
$utm = filter_var_array($contents['utm'], [
'utm_campaign' => \FILTER_SANITIZE_FULL_SPECIAL_CHARS,
'utm_source' => \FILTER_SANITIZE_FULL_SPECIAL_CHARS,
'utm_medium' => \FILTER_SANITIZE_FULL_SPECIAL_CHARS,
'utm_term' => \FILTER_SANITIZE_FULL_SPECIAL_CHARS,
'utm_content' => \FILTER_SANITIZE_FULL_SPECIAL_CHARS,
], false) ?? [];
$applicant->setUtm($utm);
}
$errors = $this->validator->validate($applicant);
if ($errors->count() > 0) {
throw new \RuntimeException($errors);
}
if (($defaultStatusObject = $this->config->get('site_applicant_default_status'))
&& $defaultStatus = $this->entityManager
->getRepository(Status::class)
->find($defaultStatusObject->getId())
) {
$applicant->setStatus($defaultStatus);
}
if ($this->featureManager->isActive(FeatureFlagListener::FEATURE_GDPR)
&& !empty($gdprId = (int) $this->request->get('gdpr'))
&& $gdprStatement = $this->entityManager
->getRepository(GdprStatement::class)
->find($gdprId)
) {
$applicant->setGdpr($gdprStatement);
}
$this->eventDispatcher->dispatch(
new ApplicantEvent($applicant, null),
ApplicantEvent::EVENT_PRE_PERSIST
);
$applicant->setFileName($applicant->getFileName());
$applicant->setFileSize($applicant->getFileSize());
$applicant->setFileNameMotivation($applicant->getFileNameMotivation());
$applicant->setFileSizeMotivation($applicant->getFileSizeMotivation());
$this->entityManager->persist($applicant);
$this->entityManager->flush();
$this->eventDispatcher->dispatch(
new ApplicantEvent($applicant, null),
ApplicantEvent::EVENT_POST_PERSIST
);
return $applicant;
}
public function addFilesFromRequest(Request $request, Applicant $applicant): Applicant
{
/**
* @var string $name
* @var UploadedFile $file
*/
foreach ($request->files as $name => $file) {
$setMethod = 'set'.$name;
if (method_exists($applicant, $setMethod)) {
$applicant->$setMethod($file);
}
}
$this->entityManager->persist($applicant);
$this->entityManager->flush();
return $applicant;
}
/**
* @throws \JsonException
*/
public function handleForm(Request $request, Applicant $applicant, Vacancy $vacancy): array
{
$multipart = [];
foreach ($request->files->all() as $files) {
foreach ($files as $name => $file) {
if (empty($file['file'])) {
continue;
}
$file = $file['file'];
/* @var UploadedFile $file */
$multipart[] = [
'name' => $name,
'contents' => fopen($file->getRealPath(), 'r'),
'filename' => $file->getClientOriginalName(),
];
}
}
foreach (json_decode($this->serializer->serialize($applicant, 'json'), false, 512, \JSON_THROW_ON_ERROR) as $field => $value) {
if ('phone' === $field) {
$value = (string) $applicant->getPhone();
}
$multipart[] = [
'name' => $field,
'contents' => $value,
];
}
$response = $this->client->post(
$this->router->generate('rest_applicant_create', ['vacancy' => $vacancy->getId()]),
[
'multipart' => $multipart,
]
);
$applicantResponse = $response->getBody()->getContents();
if (empty($applicantResponse)) {
throw new \RuntimeException('Applicant could not be posted to master site');
}
return json_decode($applicantResponse, true, 512, \JSON_THROW_ON_ERROR);
}
public function getAsyncApplicantFormForVacancy(int $id): ApplicantForm
{
$response = $this->client->get($this->router->generate('rest_vacancy_full_form', ['id' => $id]));
return $this->serializer->deserialize(
$response->getBody()->getContents(),
ApplicantForm::class,
'json'
);
}
public function getApplicantOwner(Applicant $applicant): ?Recruiter
{
$recruiter = null;
if ($vacancy = $applicant->getVacancy()) {
$recruiter = $vacancy->getRecruiter();
}
try {
$applicantData = json_decode($applicant->getData(), true, 512, \JSON_THROW_ON_ERROR);
} catch (\JsonException) {
return $recruiter;
}
if (!$recruiter && !empty($applicantData['company'])) {
/** @var Company $company */
$company = $this->entityManager->getRepository(Company::class)->find($applicantData['company']);
if ($company && !$company->getRecruiters()->isEmpty()) {
$recruiter = $company->getRecruiters()->first();
}
}
return $recruiter;
}
/**
* Generate URL for successful application with either external or CMS id.
*/
public function generateSuccessRedirect(Applicant $applicant, ?Vacancy $vacancy = null): RedirectResponse
{
return new RedirectResponse(
$this->generateSuccessRedirectUrl($applicant, $vacancy)
);
}
public function generateSuccessRedirectUrl(Applicant $applicant, ?Vacancy $vacancy = null): string
{
$parameters = [];
if ($vacancy) {
$vacancyIdForDatalayer = $vacancy->getId();
if (
$this->config->get('site_google_show_external_vacancy_id') &&
!empty($vacancy->getExternalId())
) {
$vacancyIdForDatalayer = $vacancy->getExternalId();
}
$parameters['id'] = $vacancyIdForDatalayer;
}
$applicantIdForDatalayer = $this->config->get('site_google_show_external_applicant_id') ?
$applicant->getExternalId() : $applicant->getId();
$parameters[$this->config->get('site_google_applicant_query_name')] = $applicantIdForDatalayer;
return $this->router->generate('vacancy_apply_thanks', $parameters);
}
/**
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/
public function renderECommerceScript(Request $request, ?Vacancy $vacancy): string
{
if (!$this->config->get('site_google_applicant_query_name') ||
!$request->query->has($this->config->get('site_google_applicant_query_name'))) {
// applicant query name was either not set or provided value is not available in request
return '';
}
$category = null;
if ($vacancy) {
$optionValues = $vacancy->getOptionValues();
$categoryOption = $this->stringToEntityUtil
->stringToEntity($this->config->get('site_google_category_option'));
if (!empty($categoryOption)) {
$optionValues = $optionValues->filter(
function (OptionValue $optionValue) use ($categoryOption) {
return $optionValue->getOption() && $optionValue->getOption()->getId() === $categoryOption->getId();
}
);
}
$category = $optionValues->first();
}
return $this->twig->render('scripts/external_e_commerce_script.html.twig', [
'applicant_id' => $request->query->get($this->config->get('site_google_applicant_query_name')),
'vacancy_id' => $vacancy ? $vacancy->getId() : 'OPEN_SOLLICITATIE',
'vacancy_title' => $vacancy ? $vacancy->getTitle() : 'Open sollicitatie',
'brand_name' => $vacancy && $vacancy->getCompany() ? $vacancy->getCompany()->getName() : '',
'category' => $category,
]);
}
/**
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/
public function renderHeadEndScriptForVacancyDetail(?Vacancy $vacancy): string
{
if (!$vacancy) {
return '';
}
if ($this->featureManager->isActive(FeatureFlagListener::FEATURE_INGOEDEBANEN_ADVANCED_STATISTICS)) {
return $this->twig->render('scripts/ingoedebanen_application_end_script.html.twig', [
'vacancy' => $vacancy,
]);
}
return '';
}
/**
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/
public function renderBodyEndScriptForVacancyDetail(): string
{
if ($this->featureManager->isActive(FeatureFlagListener::FEATURE_INGOEDEBANEN_ADVANCED_STATISTICS)) {
return $this->twig->render('scripts/ingoedebanen_start_script.html.twig');
}
return '';
}
public function getDataTable(Request $request, array $query = []): DataTable
{
$email = $query['email'] ?? null;
if (ApplicantTableType::class === $this->config->get('admin_applicant_table_type')) {
return $this->getArrayDataTable($request, $query);
}
return $this->getORMDataTable($request, $email);
}
private function getArrayDataTable(Request $request, array $query = []): DataTable
{
$criteria = Criteria::create();
foreach ($query as $field => $value) {
$criteria->andWhere(Criteria::expr()->eq($field, $value));
}
$applicants = $this->entityManager->getRepository(Applicant::class)
->findApplicants($criteria, ['id' => $this->config->get('site_applicant_order_dir')]);
$applicantFilters = $this->adminFilterUtil->getApplicantFilters();
$applicants = array_filter($applicants, function (Applicant $applicant) use ($applicantFilters) {
if (
!empty($applicantFilters['applicantCompany']) &&
!$this->validateCompanyOfApplicant($applicant, $applicantFilters['applicantCompany'])
) {
return false;
}
if (\in_array(ApplicantFilterType::SHOW_DELETED_APPLICANTS, $applicantFilters['filters'], true) && null !== $applicant->getDeletedAt() && $this->authorizationChecker->isGranted('view', $applicant)) {
return true;
}
if (!$applicant->getVacancy()) {
return \in_array(ApplicantFilterType::SHOW_OPEN_APPLICANTS, $applicantFilters['filters'], true)
&& $this->authorizationChecker->isGranted('view', $applicant)
&& (
null === $applicant->getDeletedAt()
|| \in_array(ApplicantFilterType::SHOW_DELETED_APPLICANTS, $applicantFilters['filters'], true)
);
}
if (\in_array(ApplicantFilterType::SHOW_REGULAR_APPLICANTS, $applicantFilters['filters'], true) && $this->authorizationChecker->isGranted('view', $applicant)) {
return !(null !== $applicant->getDeletedAt() && !\in_array(ApplicantFilterType::SHOW_DELETED_APPLICANTS, $applicantFilters['filters'], true));
}
return false;
});
$statusApplicantIdPairs = [];
$applicantStatusses = [];
if ($this->featureManager->isActive(FeatureFlagListener::FEATURE_ATS)) {
$applicantStatusses = $this->entityManager->getRepository(ApplicantStatus::class)->findAll();
$statusApplicantIdPairs = $this->entityManager->getRepository(ApplicantStatus::class)->findAllIdStatusPairs($this->adminFilterUtil->getApplicantFilters());
$statusApplicantIdPairs = array_column($statusApplicantIdPairs, 'title', 'id');
}
$applicantRepo = $this->entityManager->getRepository(Applicant::class);
$applicantsCount = $applicantRepo->findApplicationCountsPerEmail();
$dataTableData = array_map(function (Applicant $applicant) use ($applicantStatusses, $statusApplicantIdPairs, $applicantsCount) {
$applicationCount = 0;
if ($applicant->getEmail()) {
$applicationCount = $applicantsCount[$applicant->getEmail()] ?? 0;
}
$status = $applicant->getStatus() ? $applicant->getStatus()->getTitle() : null;
if (!empty($statusApplicantIdPairs[$applicant->getId()])) {
$status = $statusApplicantIdPairs[$applicant->getId()];
}
$applicantData = [
'applicant' => $applicant,
'id' => $applicant->getId(),
'externalId' => $applicant->getExternalId(),
'externalCandidateId' => $applicant->getExternalCandidateId(),
'displayableExternalId' => $applicant->getVacancy() ? $applicant->getVacancy()->getDisplayableExternalId() : null,
'name' => $applicant->getFullName(),
'status' => $status,
'email' => $applicant->getEmail(),
'phone' => $applicant->getPhone(),
'vacancyTitle' => $applicant->getVacancy() ? $applicant->getVacancy()->getTitle() : null,
'vacancyDomain' => $applicant->getVacancy() && $applicant->getVacancy()->getDomain() ? $applicant->getVacancy()->getDomain()->getName() : null,
'companyName' => $applicant->getVacancy() && $applicant->getVacancy()->getCompany() ?
$applicant->getVacancy()->getCompany()->getName() : null,
'appliedOn' => $applicant->getCreatedAt(),
'applicantStatusses' => $applicantStatusses,
'deleted' => null !== $applicant->getDeletedAt(),
'logs' => [], // $logManager->getLastObjectTransactionLog($applicant),
'applicationCount' => $applicationCount,
];
if ($this->featureManager->isActive(FeatureFlagListener::FEATURE_VACANCY_KNOCKOUT_QUESTIONS)) {
$applicantData['rejected'] = $applicant->isRejectedByKnockout() ? 'Yes' : 'No';
}
return $applicantData;
}, $applicants);
return $this->dataTableFactory->createFromType(ApplicantTableType::class, ['data' => $dataTableData])
->handleRequest($request);
}
private function validateCompanyOfApplicant(Applicant $applicant, int $companyIdToCheck): bool
{
if (!$applicant->getVacancy()) {
return false;
}
if (!$applicant->getVacancy()->getCompany()) {
return false;
}
return $applicant->getVacancy()->getCompany()->getId() === $companyIdToCheck;
}
private function getORMDataTable(Request $request, ?string $email = null): DataTable
{
return $this->dataTableFactory->createFromType(
ORMApplicantTableType::class,
[
'email' => $email,
'filters' => $this->adminFilterUtil->getApplicantFilters(),
'order_by' => $this->config->get('site_applicant_order_dir'),
]
)->handleRequest($request);
}
public function getSuccessMultiMedia(?Vacancy $vacancy = null, ?string $locale = null): ?MultiMedia
{
$defaultMultimedia = $this->config->get('applicant_success_multimedia_fallback');
if (!$vacancy) {
return $defaultMultimedia;
}
switch ($this->config->get('applicant_success_multimedia_strategy')) {
case ApplicantSettingType::MULTIMEDIA_STRATEGY_CHOICE_FALLBACK:
default:
return $defaultMultimedia;
case ApplicantSettingType::MULTIMEDIA_STRATEGY_CHOICE_OPTION:
if (
!$selectedOption = $this->config->get('applicant_success_multimedia_option')
) {
return $defaultMultimedia;
}
if (!$selectedSection = $this->config->get('applicant_success_multimedia_option_section')) {
$selectedSection = 'featured';
}
$optionValues = array_filter(
$vacancy->getOptionValues()?->toArray(),
static fn (OptionValue $optionValue) => $optionValue->getOption()?->getId() === $selectedOption->getId()
);
foreach ($optionValues as $optionValue) {
if ('featured' === $selectedSection) {
if ($multimedia = $this->multiMediaManager->getFeaturedMultiMediaByEntity($optionValue, $locale)) {
return $multimedia;
}
continue;
}
if ($multimedia = $this->multiMediaManager->getMultiMediaByEntity($optionValue, $selectedSection, $locale)) {
return reset($multimedia);
}
}
return $defaultMultimedia;
case ApplicantSettingType::MULTIMEDIA_STRATEGY_CHOICE_COMPANY:
if (!$company = $vacancy->getCompany()) {
return $defaultMultimedia;
}
if (!$selectedSection = $this->config->get('applicant_success_multimedia_company_section')) {
$selectedSection = 'featured';
}
if ('featured' === $selectedSection && $multimedia = $this->multiMediaManager->getFeaturedMultiMediaByEntity($company, $locale)) {
return $multimedia;
}
if ($multimedia = $this->multiMediaManager->getMultiMediaByEntity($company, $selectedSection, $locale)) {
return reset($multimedia);
}
return $defaultMultimedia;
}
}
}