<?php
namespace App\Reader\Vacancy;
use App\Component\Attributes\AttributeManager;
use App\Component\Attributes\AttributeManagerFactory;
use App\Component\Configuration\Util\Config;
use App\Decorator\VacancyDecorator;
use App\Decorator\VacancyOptionDecorator;
use App\Entity\Asset;
use App\Entity\Gallery;
use App\Entity\Option;
use App\Entity\OptionValue;
use App\Entity\Recruiter;
use App\Entity\RecruiterAttribute;
use App\Entity\Repository\VacancyRepository;
use App\Entity\Vacancy;
use App\Entity\VacancyAttribute;
use App\EventListener\FeatureFlagListener;
use App\FilterSet\VacancyFilterSet;
use App\Form\Setting\MultiSiteSettingType;
use App\Form\Setting\VacancySettingType;
use App\Manager\MultiMediaManager;
use App\Manager\VacancyDomainManager;
use App\Model\Vacancy\SearchResultSet;
use App\Model\Vacancy\SearchResultSetMeta;
use App\Model\Vacancy\VacancyDomainCollection;
use App\Service\SiteService;
use App\Service\VacancyService;
use App\Util\AssetUtil;
use App\Util\PhoneNumberUtil;
use App\Util\VacancyImage;
use App\Util\VacancyUtil;
use App\Vacancy\Sort\SortFactory;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\NoResultException;
use Flagception\Manager\FeatureManager;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\Request;
use Traversable;
class DoctrineReader extends AbstractVacancyReader
{
protected ?Recruiter $defaultRecruiter = null;
protected array $optionRecruiters = [];
protected ?Gallery $defaultGallery = null;
protected array $optionGalleries = [];
protected array $companyGalleries = [];
protected AttributeManager $vacancyAttributeManager;
protected AttributeManager $recruiterAttributeManager;
public function __construct(
protected EntityManagerInterface $entityManager,
protected ParameterBagInterface $parameterBag,
protected VacancyDecorator $vacancyDecorator,
protected SiteService $siteService,
protected VacancyService $vacancyService,
protected VacancyOptionDecorator $vacancyOptionDecorator,
protected VacancyFilterSet $filterSet,
protected AssetUtil $assetUtil,
protected VacancyDomainManager $vacancyDomainManager,
protected FeatureManager $featureManager,
protected MultiMediaManager $multiMediaManager,
protected Config $config,
protected VacancyImage $vacancyImage,
AttributeManagerFactory $attributeManagerFactory,
protected SortFactory $vacancySortFactory
) {
$this->vacancyAttributeManager = $attributeManagerFactory->create(VacancyAttribute::class);
$this->recruiterAttributeManager = $attributeManagerFactory->create(RecruiterAttribute::class);
}
/**
* @param string|int $id
*/
public function get($id): Vacancy
{
return $this->getRepository()->find($id);
}
/**
* @return Vacancy[]|array
*/
public function getAll(): array
{
return $this->getRepository()->findAll();
}
/**
* @throws NonUniqueResultException|NoResultException
*/
public function getSearchResultSetByRequest(Request $request): SearchResultSet
{
$domainCollection = $this->getVacancyDomainCollection($request);
$vacancyRepository = $this->getRepository();
$hostSite = null;
if ($host = $request->query->get('host')) {
$hostSite = $this->siteService->getSiteFromHost($host);
}
$offset = 0;
if ($request->query->getInt('pageNumber') > 1) {
$offset = $request->get(
'pageSize',
$this->config->get('site_vacancy_limit_per_page')
) * ($request->query->getInt('pageNumber', 1) -
1);
}
/** @var array<string, mixed> $preFilters */
$preFilters = $this->parameterBag->get('site_vacancy_overview_prefilter');
$filters = $this->vacancyService->fetchFilters($request, $preFilters);
$options = $this->vacancyService->getOptionsFromRequest($request, $preFilters);
$sort = $this->vacancySortFactory->create();
$vacancies = $vacancyRepository->findByOptionsWithFilterSet(
$options,
$this->filterSet,
$request->get('sort', $sort->getField()),
$request->get('sortDir', $sort->getDirection()),
$offset,
$request->query->getInt('pageSize', \intval($this->config->get('site_vacancy_limit_per_page'))),
$this->config->get('site_vacancy_show_internal_vacancy_on_overview'),
$hostSite ?? $this->siteService->getSite(),
$domainCollection
);
$vacancyCount = $vacancyRepository->findByOptionsWithFilterSetCount(
$options,
$this->filterSet,
$this->config->get('site_vacancy_show_internal_vacancy_on_overview'),
$hostSite ?? $this->siteService->getSite(),
$domainCollection
);
$facets = $this->vacancyService->getFacets(
$options,
$filters,
$this->filterSet,
$this->config->get('site_vacancy_show_internal_vacancy_on_overview'),
$hostSite ?? $this->siteService->getSite(),
$domainCollection
);
$this->vacancyDecorator->decorate($vacancies, false, false);
$this->vacancyOptionDecorator->decorate($vacancies);
$objectMultiMedia = [];
if ($this->featureManager->isActive(FeatureFlagListener::FEATURE_MULTI_MEDIA)) {
$objectMultiMedia = $this->multiMediaManager->getFeaturedObjectMultiMediaByClass(Vacancy::class);
}
$maxPaginationItems = 2;
$totalPageCount = ceil($vacancyCount / $this->config->get('site_vacancy_limit_per_page'));
if ($totalPageCount < $maxPaginationItems) {
$maxPaginationItems = $totalPageCount;
}
$vacancyOutput = [];
/** @var Vacancy $vacancy */
foreach ($vacancies as $vacancy) {
$data = $this->transformVacancy($vacancy, $objectMultiMedia);
$vacancyOutput[] = $data;
}
$seoSnippet = $this->vacancyService->getSeoSnippetsFromFacets($facets, $this->siteService->getSite());
unset($seoSnippet['optionValues']);
$maxSalaryRangeOptions = $options;
$maxSalaryRangeOptions['filters'] = array_filter($options['filters'], fn ($filter) => 'salary_filter' !== $filter['name']);
$maxSalaryRange = $this->featureManager->isActive(FeatureFlagListener::FEATURE_VACANCY_SALARY_RANGE_FILTER)
? \intval($vacancyRepository->findMaxSalaryAmount(
$maxSalaryRangeOptions,
$this->filterSet,
(bool) $this->config->get('site_vacancy_show_internal_vacancy_on_overview'),
$domainCollection,
) / 100)
: null;
return new SearchResultSet(
$vacancyOutput,
$facets,
new SearchResultSetMeta(
$vacancyCount,
$request->query->getInt('pageNumber', 1),
$this->config->get('site_vacancy_limit_per_page'),
$totalPageCount,
$maxPaginationItems,
$seoSnippet,
$this->featureManager->isActive(FeatureFlagListener::FEATURE_TEALIUM) ?
$this->getTealiumData($request, $vacancyCount)->toArray() : [],
$maxSalaryRange
)
);
}
/**
* @throws NonUniqueResultException|NoResultException
*/
public function getMapSearchResultSetByRequest(Request $request): SearchResultSet
{
$domainCollection = $this->getVacancyDomainCollection($request);
$vacancyRepository = $this->getRepository();
$hostSite = null;
if ($host = $request->query->get('host')) {
$hostSite = $this->siteService->getSiteFromHost($host);
}
$offset = 0;
/** @var array $preFilters */
$preFilters = $this->parameterBag->get('site_vacancy_overview_prefilter');
$filters = $this->vacancyService->fetchFilters($request, $preFilters);
$options = $this->vacancyService->getOptionsFromRequest($request, $preFilters);
$sort = $this->vacancySortFactory->create();
/** @var Vacancy[] $vacancies */
$vacancies = $vacancyRepository->findMapByOptionsWithFilterSet(
$options,
$this->filterSet,
$request->get('sort', $sort->getField()),
$request->get('sortDir', $sort->getDirection()),
$offset,
9999,
$this->config->get('site_vacancy_show_internal_vacancy_on_overview'),
$hostSite ?? $this->siteService->getSite(),
$domainCollection
);
$facets = [];
$seoSnippet = [];
return new SearchResultSet(
$vacancies,
$facets,
new SearchResultSetMeta(
9,
1,
1,
1,
0,
$seoSnippet,
[]
)
);
}
public function transformVacancy(Vacancy $vacancy, array $objectMultiMedia): array
{
$company = $vacancy->getCompany();
$featuredMultiMedia = $this->getFeaturedMultiMedia($objectMultiMedia, $vacancy->getId());
$data = [
'id' => $vacancy->getId(),
'external_id' => $vacancy->getExternalId(),
'external_reference' => md5($vacancy->getExternalReference() ?? ''),
'displayable_external_id' => $vacancy->getDisplayableExternalId(),
'created' => $vacancy->getCreated(),
'updated' => $vacancy->getUpdated(),
'slug' => $vacancy->getSlug(),
'title' => $vacancy->getTitle(),
'subtitle' => $vacancy->getSubTitle(),
'city' => $vacancy->getCity(),
'salary' => $vacancy->getSalary(),
'min_salary' => $vacancy->getMinSalary(),
'max_salary' => $vacancy->getMaxSalary(),
'salary_unit' => $vacancy->getSalaryUnit(),
'zipcode' => $vacancy->getZipcode(),
'intro' => $vacancy->getIntro(),
'option_values' => $this->parseOptionValues($vacancy->getOptionValues()),
'main_icon' => [
'font_awesome_icon' => $vacancy->getMainIcon() ? $vacancy->getMainIcon()->getFontAwesomeIcon() : '',
],
'vacancy_usp_values' => [],
'usps' => [],
'featured' => $vacancy->isFeatured(),
'internal_vacancy' => $vacancy->isInternalVacancy(),
'new' => $vacancy->isNew(),
'address' => $vacancy->getAddress(),
'latitude' => $vacancy->getLatitude(),
'longitude' => $vacancy->getLongitude(),
'overview_images' => array_map(
fn (Asset $asset) => $this->assetUtil->getAssetUrlsForFilters($asset),
$vacancy->getOverviewImages()
),
'featured_multi_media' => $featuredMultiMedia,
'custom_salary' => $this->config->get('site_vacancy_show_custom_salary_in_overview') ? $vacancy->getCustomSalary() : null,
'show_city' => $this->config->get('site_vacancy_show_location'),
'analytics_data' => [
'favorite' => json_encode([
'elementAction' => 'favorite',
'jobId' => $vacancyAnalyticsData['job_id'] ?? $vacancy->getExternalId(),
'jobName' => $vacancyAnalyticsData['job_name'] ?? $vacancy->getTitle(),
'jobField' => $vacancyAnalyticsData['job_field'] ?? '',
'jobEntity' => $vacancyAnalyticsData['job_entity'] ?? '',
'jobCountry' => $vacancyAnalyticsData['job_country'] ?? $vacancy->getCountry(),
'jobCity' => $vacancyAnalyticsData['job_city'] ?? $vacancy->getCity(),
]),
],
'gallery' => $this->getVacancyGallery($vacancy),
'labels' => $vacancy->getLabels(),
'job_start_date' => $vacancy->getJobStartDate(),
'attribute_values' => $this->vacancyAttributeManager->getNormalizedValues($vacancy),
];
if ($this->config->get('site_vacancy_recruiter_in_overview')) {
if ($vacancy->getRecruiter()) {
$data['recruiter'] = $this->transformRecruiter($vacancy->getRecruiter());
}
if (empty($data['recruiter']) && $recruiter = $this->getVacancyRecruiter($vacancy)) {
$data['recruiter'] = $this->transformRecruiter($recruiter);
}
}
if ($company) {
$data['company'] = [
'id' => $company->getId(),
'slug' => $company->getSlug(),
'name' => $vacancy->getCompanyName() ?? $company->getName(),
'zipcode' => $company->getZipcode(),
'city' => $company->getCity(),
'website' => $company->getWebsite(),
'email' => $company->getEmail(),
'option_values' => [],
'address' => $company->getAddress(),
'latitude' => $company->getLatitude(),
'longitude' => $company->getLongitude(),
'logo' => $company->getLogo() ?
$this->assetUtil->getAssetUrlsForFilters($company->getLogo()) : null,
];
}
return $data;
}
protected function getRepository(): VacancyRepository
{
return $this->entityManager->getRepository(Vacancy::class);
}
public function isCacheAble(): bool
{
return true;
}
public function getRelatedVacancies(Request $request, Vacancy $vacancy, ?int $limit = null): array
{
$site = null;
$host = $request->query->get('host');
if (\is_string($host)) {
$site = $this->siteService->getSiteFromHost($host);
}
if (!$site) {
$site = $this->siteService->getSite();
}
$vacancyRepository = $this->entityManager->getRepository(Vacancy::class);
switch ($this->config->get('site_vacancy_related_vacancy_type')) {
case 'featured':
$relatedVacancies = $vacancy->isFeatured() ?
$vacancyRepository->findTopVacanciesOfCompany(
$vacancy->getCompany(),
[$vacancy],
$limit,
$site
) :
$vacancyRepository->findTopVacancies([$vacancy], $limit, $site);
break;
case 'options':
$relatedVacancyOptions = $this->config->get('site_vacancy_related_vacancy_options');
$relatedVacancyOptionsIds = array_map(static fn (Option $option) => $option->getId(), $relatedVacancyOptions->toArray());
$relatedVacanciesOptionValues = $vacancy->getOptionValues()->filter(function (
OptionValue $optionValue
) use ($relatedVacancyOptionsIds) {
return \in_array($optionValue->getOption()->getId(), $relatedVacancyOptionsIds, true);
});
$relatedVacancies = $vacancyRepository->findRelatedVacanciesByOptionValues(
$relatedVacanciesOptionValues->toArray(),
$limit,
$vacancy,
$site,
$this->config->get('site_vacancy_restricted_related_vacancies_to_company') ? $vacancy->getCompany() : null,
);
break;
case 'primary_discipline':
$relatedVacancies = $vacancyRepository->findRelatedByPrimaryDiscipline(
$vacancy,
$limit,
$site
);
break;
case 'primary_secundary_discipline':
$relatedVacancies = $vacancyRepository->findRelatedByPrimaryOrSecundaryDiscipline(
$vacancy,
$limit,
$site
);
break;
case 'location':
$relatedVacancies = $vacancyRepository->findRelatedByLocation(
$vacancy,
$limit,
$site
);
break;
default:
$relatedVacancies = [];
break;
}
return $relatedVacancies;
}
public function getVacancyCount(Request $request): int
{
$hostSite = null;
if ($host = $request->getHost()) {
$hostSite = $this->siteService->getSiteFromHost($host);
}
/** @var VacancyRepository $vacancyRepository */
$vacancyRepository = $this->entityManager->getRepository(Vacancy::class);
/** @var array<string, mixed> $preFilters */
$preFilters = $this->parameterBag->get('site_vacancy_overview_prefilter');
$options = $this->vacancyService->getOptionsFromRequest($request, $preFilters);
try {
return $vacancyRepository->findByOptionsWithFilterSetCount(
$options,
$this->filterSet,
true,
$hostSite
);
} catch (NonUniqueResultException|NoResultException) {
return 0;
}
}
public function getFeaturedVacancies(?int $limit = null, ?string $sort = null): array
{
if (
MultiSiteSettingType::MULTI_SITE_VACANCY_FILTER_MULTIPLE_SITES === $this->parameterBag->get('site_vacancy_filter_strategy')
) {
return $this->getRepository()->findTopVacanciesFindableOnSites([], $limit, $this->siteService->getSite(), $sort);
}
return $this->getRepository()->findTopVacancies([], $limit, $this->siteService->getSite(), $sort);
}
public function getVacanciesByIds(array $ids): array
{
$vacancies = $this->getRepository()
->findVacanciesByIds($ids)
;
return [
'vacancies' => $vacancies,
'companyLogos' => [],
];
}
protected function getVacancyDomainCollection(Request $request): VacancyDomainCollection
{
$domains = explode(';', $request->get('domains', 'public'));
$isPublicDomain = \in_array('public', $domains, true);
$isPrivateDomain = \in_array('private', $domains, true);
return $this->vacancyDomainManager->calculateVacancyDomains($isPublicDomain, $isPrivateDomain);
}
protected function getFeaturedMultiMedia(array $objectMultiMedia, int $vacancyId): array
{
return $this->vacancyService->getFeaturedMultiMedia($objectMultiMedia, $vacancyId);
}
protected function parseOptionValues(Traversable $optionValueCollection): array
{
$optionValues = [];
/** @var OptionValue $optionValue */
foreach ($optionValueCollection as $optionValue) {
$formattedOptionValue = [
'id' => $optionValue->getId(),
'value' => $optionValue->getValue(),
'sub_title' => $optionValue->getSubTitle(),
'slug' => $optionValue->getSlug(),
'font_awesome_icon' => $optionValue->getFontAwesomeIcon(),
'content' => $optionValue->getContent(),
'hasDetailPage' => $optionValue->hasDetailPage(),
'attributes' => $optionValue->getAttributes(),
'vacancy_images' => array_map(function ($image) {return $this->assetUtil->getAssetUrlsForFilters($image); }, $optionValue->getVacancyImages()->toArray()),
];
if (($option = $optionValue->getOption()) instanceof Option) {
$formattedOptionValue['option'] = [
'id' => $option->getId(),
'title' => $option->getTitle(),
'slug' => $option->getSlug(),
'font_awesome_icon' => $option->getFontAwesomeIcon(),
'expanded_by_default' => $option->isExpandedByDefault(),
'strategy_on_overview' => $option->getStrategyOnOverview(),
'visible_in_detail' => $option->isVisibleInDetail(),
'values' => [],
'position' => $option->getPosition(),
'internal_name' => $option->getInternalName(),
'display_type' => $option->getDisplayType(),
];
}
$optionValues[] = $formattedOptionValue;
}
return $optionValues;
}
protected function transformRecruiter(Recruiter $recruiter): array
{
return [
'name' => $recruiter->getName(),
'first_name' => $recruiter->getFirstName(),
'last_name_prefix' => $recruiter->getLastNamePrefix(),
'last_name' => $recruiter->getLastName(),
'email' => $recruiter->getEmail(),
'phone' => [
'e164' => PhoneNumberUtil::formatPhoneE164($recruiter->getPhone()),
'regular' => PhoneNumberUtil::formatPhoneNational($recruiter->getPhone()),
],
'picture' => $this->assetUtil->getFilteredUrlsForObject(
$recruiter,
'picture',
[
'avatar_48',
'avatar_64',
'avatar_96',
]
),
'attribute_values' => $this->recruiterAttributeManager->getNormalizedValues($recruiter),
];
}
private function getDefaultRecruiter(): ?Recruiter
{
if (!$this->defaultRecruiter) {
$recruiterFallback = $this->config->get('site_vacancy_detail_recruiter_fallback');
if ($recruiterFallback) {
$this->defaultRecruiter = $this->entityManager
->getRepository(Recruiter::class)
->find($recruiterFallback->getId());
}
}
return $this->defaultRecruiter;
}
private function getOptionRecruiter(Vacancy $vacancy): ?Recruiter
{
$recruiterOption = $this->config->get('site_vacancy_detail_recruiter_option');
if (!$recruiterOption) {
return null;
}
if (empty($this->optionRecruiters[$recruiterOption->getId()])) {
$this->optionRecruiters[$recruiterOption->getId()] = null;
$recruiterOptionOptionValues = VacancyUtil::getOptionValuesByOptionId($vacancy, $recruiterOption->getId());
$recruiterOptionOptionValue = $recruiterOptionOptionValues->first();
if ($recruiterOptionOptionValue) {
$this->optionRecruiters[$recruiterOption->getId()] = $recruiterOptionOptionValue->getRecruiter();
}
}
return $this->optionRecruiters[$recruiterOption->getId()];
}
protected function getVacancyRecruiter(Vacancy $vacancy): ?Recruiter
{
return match ($this->config->get('site_vacancy_detail_recruiter_strategy')) {
VacancySettingType::VACANCY_DETAIL_RECRUITER_STRATEGY_OPTION => $this->getOptionRecruiter($vacancy),
VacancySettingType::VACANCY_DETAIL_RECRUITER_STRATEGY_FALLBACK => $this->getDefaultRecruiter(),
default => null,
};
}
private function getDefaultGallery(): ?Gallery
{
if (!$this->defaultGallery) {
$galleryFallback = $this->config->get('site_vacancy_detail_gallery_fallback');
if ($galleryFallback) {
$this->defaultGallery = $this->entityManager
->getRepository(Gallery::class)
->find($galleryFallback->getId());
}
}
return $this->defaultGallery;
}
private function getOptionGallery(Vacancy $vacancy): ?Gallery
{
$galleryOption = $this->config->get('site_vacancy_detail_gallery_option');
if (!$galleryOption) {
return null;
}
if (empty($this->optionGalleries[$galleryOption->getId()])) {
$this->optionGalleries[$galleryOption->getId()] = null;
$galleryOptionOptionValues = VacancyUtil::getOptionValuesByOptionId($vacancy, $galleryOption->getId());
$galleryOptionOptionValue = $galleryOptionOptionValues->first();
if ($galleryOptionOptionValue) {
$this->optionGalleries[$galleryOption->getId()] = $galleryOptionOptionValue->getGallery();
}
}
return $this->optionGalleries[$galleryOption->getId()];
}
private function getCompanyGallery(Vacancy $vacancy): ?Gallery
{
$galleryCompany = $vacancy->getCompany();
if (!$galleryCompany) {
return null;
}
if (empty($this->companyGalleries[$galleryCompany->getId()])) {
$this->companyGalleries[$galleryCompany->getId()] = $galleryCompany->getGallery();
}
return $this->companyGalleries[$galleryCompany->getId()];
}
protected function getVacancyGallery(Vacancy $vacancy): ?Gallery
{
switch ($this->config->get('site_vacancy_detail_gallery_strategy')) {
case VacancySettingType::VACANCY_DETAIL_GALLERY_STRATEGY_NONE:
default:
return null;
case VacancySettingType::VACANCY_DETAIL_GALLERY_STRATEGY_COMPANY:
return $this->getCompanyGallery($vacancy);
case VacancySettingType::VACANCY_DETAIL_GALLERY_STRATEGY_OPTION:
return $this->getOptionGallery($vacancy);
case VacancySettingType::VACANCY_DETAIL_GALLERY_STRATEGY_FALLBACK:
return $this->getDefaultGallery();
}
}
}