<?php
namespace App\Service;
use App\Client\GoogleAnalyticsClient;
use App\Client\MultiSiteClient;
use App\Component\Attributes\AttributeManager;
use App\Component\Attributes\AttributeManagerFactory;
use App\Component\Configuration\Util\Config;
use App\Decorator\VacancyDecorator;
use App\Entity\Asset;
use App\Entity\Company;
use App\Entity\ObjectMultiMedia;
use App\Entity\OgImage;
use App\Entity\Option;
use App\Entity\OptionValue;
use App\Entity\OptionValueAttribute;
use App\Entity\Repository\VacancyRepository;
use App\Entity\SeoSnippet;
use App\Entity\Site;
use App\Entity\Tag;
use App\Entity\User;
use App\Entity\Vacancy;
use App\Entity\VacancyAttribute;
use App\Entity\VacancyDescriptionForm;
use App\Entity\VacancyUspValue;
use App\EventListener\FeatureFlagListener;
use App\FilterSet\FilterSetInterface;
use App\FilterSet\VacancyFilterSet;
use App\Form\Setting\VacancySettingType;
use App\Manager\FilterManager;
use App\Manager\MultiMediaManager;
use App\Manager\OptionManager;
use App\Manager\VacancyDomainManager;
use App\Model\Vacancy\QueryContext;
use App\Model\Vacancy\SearchResultSet;
use App\Model\Vacancy\VacancyDomainCollection;
use App\Model\Vacancy\VacancyWrapper;
use App\Reader\Vacancy\AbstractVacancyReader;
use App\Reader\Vacancy\DoctrineReader;
use App\Reader\Vacancy\GoogleCloudTalentSolutionReader;
use App\Reader\Vacancy\MultilingualDoctrineReader;
use App\Reader\Vacancy\MultiSiteReader;
use App\Reader\Vacancy\VacancyReaderCollection;
use App\Templating\Decorator;
use App\Translation\TranslationUtil;
use App\Util\ArrayUtil;
use App\Util\AssetUtil;
use App\Util\Seo;
use App\Util\VacancyImage;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NonUniqueResultException;
use Exception;
use Flagception\Manager\FeatureManagerInterface;
use Gedmo\Translatable\TranslatableListener;
use GuzzleHttp\Client;
use JMS\Serializer\Serializer;
use JMS\Serializer\SerializerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Twig\Environment as Twig;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
class VacancyService
{
protected $imageSizes = [
'w360xh420',
'w240',
'w240_parent',
'original_parent',
'w480',
'w580',
'original',
'square_100x100_crop',
'w480xh240',
'w992xh550',
'scale_100x100',
'w330xh150',
'w767xh575',
'w480_thumbnail',
'w768xh432_crop',
];
protected AttributeManager $optionValueAttributeManager;
/**
* @var FormFactoryInterface
*/
private $formBuilder;
/**
* @var EntityManagerInterface
*/
private $entityManager;
/**
* @var array
*/
private $possibleSortOptions;
/**
* @var ParameterBagInterface
*/
private $parameterBag;
/**
* @var Twig
*/
private $twig;
/**
* @var Decorator
*/
private $decorator;
/**
* @var UrlGeneratorInterface
*/
private $urlGenerator;
/**
* @var TranslatableListener|null
*/
private $translatableListener;
/**
* @var VacancyDecorator
*/
private $vacancyDecorator;
/**
* @var RouterInterface
*/
private $router;
/**
* @var Serializer
*/
private $serializer;
/**
* @var SiteService
*/
private $siteService;
/**
* @var Client
*/
private $client;
/**
* @var FeatureManagerInterface
*/
private $featureManager;
/**
* @var Request|null
*/
private $request;
/**
* @var CreditService
*/
private $creditService;
/**
* @var Seo
*/
private $seo;
/**
* @var VacancyImage
*/
private $vacancyImage;
/**
* @var AssetUtil
*/
private $assetUtil;
/**
* @var VacancyReaderCollection
*/
private $readerCollection;
/**
* @var Site[]|null
*/
private $sites;
/**
* @var AuthorizationCheckerInterface
*/
private $authorizationChecker;
/**
* @var TokenStorageInterface
*/
private $tokenStorage;
/**
* @var TranslationUtil
*/
private $translationUtil;
private OptionManager $optionManager;
private FilterManager $filterManager;
private VacancyDomainManager $vacancyDomainManager;
private VacancyFilterSet $filterSet;
/**
* @var EntityManagerInterface
*/
private $parentEntityManager;
protected MultiMediaManager $multiMediaManager;
protected GoogleAnalyticsClient $analyticsClient;
protected GoogleAnalyticsService $googleAnalyticsService;
private Config $config;
private AttributeManager $attributeManager;
/**
* Service constructor.
*/
public function __construct(
FormFactoryInterface $formFactory,
EntityManagerInterface $entityManager,
ParameterBagInterface $parameterBag,
Twig $twig,
Decorator $decorator,
UrlGeneratorInterface $urlGenerator,
VacancyDecorator $vacancyDecorator,
RouterInterface $router,
SerializerInterface $serializer,
SiteService $siteService,
MultiSiteClient $multiSiteClient,
FeatureManagerInterface $featureManager,
RequestStack $requestStack,
CreditService $creditService,
Seo $seo,
VacancyImage $vacancyImage,
AssetUtil $assetUtil,
VacancyReaderCollection $readerCollection,
AuthorizationCheckerInterface $authorizationChecker,
TokenStorageInterface $tokenStorage,
TranslationUtil $translationUtil,
OptionManager $optionManager,
FilterManager $filterManager,
VacancyDomainManager $vacancyDomainManager,
?TranslatableListener $translatableListener,
VacancyFilterSet $filterSet,
EntityManagerInterface $parentEntityManager,
MultiMediaManager $multiMediaManager,
GoogleAnalyticsClient $analyticsClient,
GoogleAnalyticsService $googleAnalyticsService,
Config $config,
AttributeManagerFactory $attributeManagerFactory
) {
$this->formBuilder = $formFactory;
$this->entityManager = $entityManager;
$this->possibleSortOptions = $parameterBag->get('vacancy.sorting_options');
$this->parameterBag = $parameterBag;
$this->twig = $twig;
$this->decorator = $decorator;
$this->urlGenerator = $urlGenerator;
$this->vacancyDecorator = $vacancyDecorator;
$this->router = $router;
$this->serializer = $serializer;
$this->siteService = $siteService;
$this->client = $multiSiteClient;
$this->featureManager = $featureManager;
$this->request = $requestStack->getCurrentRequest();
$this->creditService = $creditService;
$this->seo = $seo;
$this->vacancyImage = $vacancyImage;
$this->assetUtil = $assetUtil;
$this->readerCollection = $readerCollection;
$this->authorizationChecker = $authorizationChecker;
$this->tokenStorage = $tokenStorage;
$this->translatableListener = $translatableListener;
$this->translationUtil = $translationUtil;
$this->optionManager = $optionManager;
$this->filterManager = $filterManager;
$this->vacancyDomainManager = $vacancyDomainManager;
$this->filterSet = $filterSet;
$this->parentEntityManager = $parentEntityManager;
$this->multiMediaManager = $multiMediaManager;
$this->analyticsClient = $analyticsClient;
$this->googleAnalyticsService = $googleAnalyticsService;
$this->config = $config;
$this->attributeManager = $attributeManagerFactory->create(VacancyAttribute::class);
$this->optionValueAttributeManager = $attributeManagerFactory->create(OptionValueAttribute::class);
}
public function getSortingForm(): FormInterface
{
$siteSortOptions = $this->possibleSortOptions;
return $this->formBuilder->createBuilder()
->add('sortBy', ChoiceType::class, [
'choices' => array_flip($siteSortOptions),
'label' => 'Sorteren op',
])
->getForm();
}
public function processOptionValueIndex(int $optionValueId, int $newIndex)
{
$optionValue = $this->entityManager->getRepository(OptionValue::class)->find($optionValueId);
$optionValue->setPosition($newIndex);
$this->entityManager->persist($optionValue);
$this->entityManager->flush();
}
public function processVacancyUspValueIndex(int $vacancyUspValueId, int $newIndex)
{
$vacancyUspValue = $this->entityManager->getRepository(VacancyUspValue::class)->find($vacancyUspValueId);
$vacancyUspValue->setPosition($newIndex);
$this->entityManager->persist($vacancyUspValue);
$this->entityManager->flush();
}
public function getFacets(
array $options,
array $filters,
FilterSetInterface $filterSet,
bool $showInternalVacancies = true,
?Site $site = null,
?VacancyDomainCollection $domainCollection = null
): array {
$locale = null;
$entityManager = $this->getEntityManager();
if ($this->translatableListener instanceof TranslatableListener) {
$locale = $this->translatableListener->getListenerLocale();
$facetCounts = $entityManager
->getRepository(Vacancy::class)->findTranslatedByOptionsWithFilterSetFacets(
$options,
$filterSet,
$showInternalVacancies,
$locale,
$site,
$this->parameterBag->get('site_translation_fallback'),
$domainCollection
);
} else {
$facetCounts = $entityManager
->getRepository(Vacancy::class)->findByOptionsWithFilterSetFacets(
$options,
$filterSet,
$showInternalVacancies,
$locale,
$site,
$domainCollection
);
}
$data = $this->transformFacetCount([], $facetCounts, $filters);
foreach ($data as $optionValueKey => $optionValue) {
foreach ($optionValue['values'] as $optionValueValue) {
if ($optionValueValue['selected']) {
$data = $this->getFacetsForOption(
$optionValue['title'],
$data,
$options,
$filters,
$filterSet,
$showInternalVacancies,
$locale,
$site,
$domainCollection
);
break;
}
}
if (Option::OPTION_DISPLAY_TYPE_DROPDOWN === $optionValue['display_type']) {
$data[$optionValueKey]['dropdown_title'] = $optionValue['title'];
if ($optionValueValue['selected']) {
$data[$optionValueKey]['dropdown_title'] = $optionValueValue['value'];
}
}
$this->sortByPosition($data[$optionValueKey]['values']);
}
$data = array_values($data);
$this->sortByPosition($data);
return $data;
}
/**
* @param $data
*/
public function sortByPosition(&$data)
{
usort($data, function ($optionA, $optionB) {
if ($optionA['position'] === $optionB['position']) {
return 0;
}
return $optionA['position'] < $optionB['position'] ? -1 : 1;
});
}
public function getFacetsForOption(
string $optionName,
array $data,
array $options,
array $filters,
FilterSetInterface $filterSet,
bool $showInternalVacancies,
?string $locale = null,
?Site $site = null,
?VacancyDomainCollection $vacancyDomainCollection = null
): array {
$data[$optionName]['values'] = [];
$entityManager = $this->getEntityManager();
foreach ($options['filters'] as $index => $option) {
if ('filterquery' === $option['name'] && isset($options['filters'][$index]['value'][$optionName])) {
unset($options['filters'][$index]['value'][$optionName]);
}
if (!$options['filters'][$index]['value']) {
unset($options['filters'][$index]);
}
}
if ($this->translatableListener instanceof TranslatableListener) {
$facetCounts = $entityManager->getRepository(Vacancy::class)
->findTranslatedByOptionsWithFilterSetFacets(
$options,
$filterSet,
$showInternalVacancies,
$locale,
$site,
$this->parameterBag->get('site_translation_fallback'),
$vacancyDomainCollection
);
} else {
$facetCounts = $entityManager->getRepository(Vacancy::class)
->findByOptionsWithFilterSetFacets(
$options,
$filterSet,
$showInternalVacancies,
$locale,
$site,
$vacancyDomainCollection
);
}
return $this->transformFacetCount($data, $facetCounts, $filters, $optionName);
}
public function transformFacetCount(
array $data,
array $facetCounts,
array $filters,
?string $excludedOptionName = null
): array {
if ($this->config->get('site_vacancy_overview_facet_show_zero_results')) {
$locale = $this->translatableListener?->getListenerLocale();
$facetCountsIds = array_column($facetCounts, 'id');
$optionValues = $this->entityManager->getRepository(OptionValue::class)->findFacetOptionValues($locale);
foreach ($optionValues as $optionValue) {
if (\in_array($optionValue['id'], $facetCountsIds, true)) {
continue;
}
$optionValue['count'] = 0;
$optionValue['optionValueVacancyImageFileName'] = false;
$facetCounts[] = $optionValue;
}
}
foreach ($facetCounts as $facetCount) {
if ($excludedOptionName && $facetCount['title'] !== $excludedOptionName) {
continue;
}
if (!isset($data[$facetCount['title']])) {
$data[$facetCount['title']] = [
'id' => $facetCount['option_id'],
'title' => $facetCount['title'],
'internal_name' => $facetCount['internalName'],
'contextual_title' => $facetCount['contextualTitle'] ?? null,
'slug' => $facetCount['slug'],
'expanded_by_default' => $facetCount['expandedByDefault'],
'font_awesome_icon' => $facetCount['fontAwesomeIcon'] ?? null,
'position' => $facetCount['optionPosition'],
'display_type' => $facetCount['display_type'] ?? null,
'expanded_strategy' => $facetCount['expanded_strategy'] ?? null,
'strategy_on_overview' => $facetCount['strategyOnOverview'] ?? null,
];
}
$isSelected = false;
if (\array_key_exists($facetCount['title'], $filters) && \is_array($filters[$facetCount['title']])) {
$isSelected = \in_array($facetCount['value'], $filters[$facetCount['title']], true);
}
if (\array_key_exists($facetCount['title'], $filters) && \is_scalar($filters[$facetCount['title']])) {
$isSelected = $facetCount['value'] === $filters[$facetCount['title']];
}
$data[$facetCount['title']]['values'][] = [
'id' => $facetCount['id'],
// We use name or value for the same value in the application, so add both
'name' => $facetCount['value'],
'value' => $facetCount['value'],
'title' => $facetCount['optionValueTitle'] ?? null,
// We use both count and vacancyCount throughout the system, so add both
'count' => (int) $facetCount['count'],
'vacancyCount' => (int) $facetCount['count'],
'slug' => $facetCount['optionValueSlug'],
'content' => '',
'position' => $facetCount['optionValuePosition'],
'selected' => $isSelected,
'detail_page' => $facetCount['detail_page'],
'attributes' => $this->optionValueAttributeManager->getNormalizedValuesByClassAndId(OptionValue::class, $facetCount['id']),
];
}
foreach ($data as &$option) {
$option['values_count'] = \count($option['values']);
}
return $data;
}
/**
* @throws Exception When datetime cannot be initialised
*/
public function copy(Vacancy $vacancy): Vacancy
{
// clone current vacancy
$newVacancy = clone $vacancy;
$newVacancy->setEndDate(new DateTime(
sprintf('+%s days', $this->config->get('site_vacancy_lifespan_days'))
));
return $newVacancy;
}
public function renderLatestVacancies(int $limit = 50, ?string $locale = null, ?string $template = null): string
{
$entityManager = $this->getEntityManager();
$vacancyRepository = $entityManager->getRepository(Vacancy::class);
$vacancies = $vacancyRepository->findLatestVacancies(
$limit,
$this->parameterBag->get('site_vacancy_overview_prefilter'),
$locale
);
$this->vacancyDecorator->decorate($vacancies, false);
$response = '';
try {
$response = $this->twig->render(
$template ? '@default/pages/'.$template : '@default/pages/vacancy_latest.html.twig',
[
'vacancies' => $vacancies,
]
);
} catch (LoaderError|RuntimeError|SyntaxError $exception) {
}
return $response;
}
/**
* @param array $preFilters
*/
public function getOptionsFromRequest(Request $request, ?array $preFilters = []): array
{
return $this->filterManager->getOptionsFromRequest($request, $preFilters);
}
public function fetchFilters(Request $request, array $preFilters): array
{
return $this->optionManager->fetchFilters($request, $preFilters);
}
public function fetchFiltersFromUrl(Request $request): array
{
return $this->optionManager->fetchFiltersFromUrl($request);
}
public function getVacancyFromRequest(Request $request): ?VacancyWrapper
{
if ($this->translatableListener instanceof TranslatableListener) {
$this->translatableListener->setTranslatableLocale($request->getLocale());
}
if (!$request->get('id')) {
return null;
}
$vacancyRepository = $this->getEntityManager()->getRepository(Vacancy::class);
if ($vacancy = $vacancyRepository->find($request->get('id'), null, null, true)) {
$this->translationUtil->translateEntity($vacancy, $request->getLocale());
$wrapper = new VacancyWrapper();
$wrapper->vacancy = $vacancy;
return $wrapper;
}
if (
$this->featureManager->isActive(FeatureFlagListener::FEATURE_CHILD_SITE) &&
$this->parameterBag->has('site_master_host_url')
) {
$vacancyJsonString = $this->getVacancyDetailResponse($request);
if (!$vacancyJsonString) {
return null;
}
$vacancy = !empty($vacancyJsonString) ?
$this->serializer->deserialize($vacancyJsonString, Vacancy::class, 'json') :
null;
$wrapper = new VacancyWrapper();
$wrapper->vacancy = $vacancy;
$vacancyJson = json_decode($vacancyJsonString, true);
if (!empty($vacancyJson['company']) && !empty($vacancyJson['company']['logo'])) {
$wrapper->vacancyImages->companyLogo->filtersWithUrls = $vacancyJson['company']['logo'];
}
if (!empty($vacancyJson['overview_image'])) {
$wrapper->vacancyImages->overviewImage->filtersWithUrls = $vacancyJson['overview_image'];
}
if (!empty($vacancyJson['detail_image'])) {
$wrapper->vacancyImages->detailImage->filtersWithUrls = $vacancyJson['detail_image'];
}
if (!empty($vacancyJson['company']) && !empty($vacancyJson['company']['overview_image'])) {
$wrapper->vacancyImages->companyOverViewImage->filtersWithUrls = $vacancyJson['company']['overview_image'];
}
if (!empty($vacancyJson['company']) && !empty($vacancyJson['company']['hero'])) {
$wrapper->vacancyImages->companyHeroImage->filtersWithUrls = $vacancyJson['company']['hero'];
}
if (!empty($vacancyJson['hero'])) {
$wrapper->vacancyImages->heroImage->filtersWithUrls = $vacancyJson['hero'];
}
return $wrapper;
}
return null;
}
private function getVacancyDetailResponse(Request $request): ?string
{
try {
$response = $this->client->get($this->router->generate(
'app_rest_vacancy_getvacancy',
['id' => $request->get('id'), 'host' => $request->getHost()]
));
$vacancyJsonString = $response->getBody()->getContents();
} catch (Exception $exception) {
$vacancyJsonString = null;
}
return $vacancyJsonString;
}
/**
* @param Vacancy|null $vacancy
*/
public function getRelatedVacancies(Request $request, Vacancy $vacancy, ?int $limit = null): array
{
return $this->getVacancyReader()->getRelatedVacancies($request, $vacancy, $limit);
}
public function getVacancyCanonicalUrl(Vacancy $vacancy): string
{
if (!$vacancy->getSite() && !empty($this->parameterBag->get('site_master_host_url'))) {
$result = $this->client->get($this->router->generate('rest_vacancy_site', ['id' => $vacancy->getId()]));
$siteJson = $result->getBody()->getContents();
$site = null;
if (!empty($siteJson) && json_decode($siteJson)) {
$site = $this->serializer->deserialize($siteJson, Site::class, 'json');
}
$vacancy->setSite($site);
}
return $this->siteService->generateRoute(
'vacancy_detail',
$vacancy->getSite(),
['id' => $vacancy->getId(), 'slug' => $vacancy->getSlug()]
);
}
/**
* @return Vacancy[]
*/
public function getTopVacancies(?int $limit = null, ?string $sort = null): array
{
return $this->getVacancyReader()->getFeaturedVacancies($limit, $sort);
}
public function setCompanyAddress(Vacancy $vacancy): Vacancy
{
if (($company = $vacancy->getCompany()) instanceof Company) {
$vacancy->setAddress($company->getAddress());
$vacancy->setZipcode($company->getZipcode());
$vacancy->setCity($company->getCity());
}
return $vacancy;
}
/**
* @throws Exception
*/
public function renewSortDate(Vacancy $vacancy): Vacancy
{
$vacancy->setSortDate(new DateTime());
$this->entityManager->persist($vacancy);
$this->entityManager->flush();
return $vacancy;
}
public function addRedirectClick(Vacancy $vacancy)
{
$vacancy = $vacancy->addRedirect();
$this->entityManager->persist($vacancy);
$this->entityManager->flush();
}
public function getFreePosting(User $user, ?Company $company = null): bool
{
/* @var User $user */
if (!$company instanceof Company) {
$company = $user->getCompany();
}
$freePostingStatuses = $this->creditService->getFreePostingStatuses();
return $company->getCompanyStatus() && \in_array($company->getCompanyStatus(), $freePostingStatuses, true);
}
/**
* @return array|bool
*/
public function getRelatedOverviewMeta(Vacancy $vacancy): ?array
{
$mainOption = false;
$mainOptionValue = false;
// use primary discipline instead of option
if ($this->config->get('site_vacancy_use_primary_discipline_instead_of_option_for_related_overview')) {
if (null === $vacancy->getPrimaryDiscipline()) {
return null;
}
$relatedOverviewPath = $this->siteService->generateRoute(
'vacancies_with_filter',
$vacancy->getPrimaryDiscipline()->getSite(),
[
'filters' => $vacancy->getPrimaryDiscipline()->getOption()->getSlug().'/'.$vacancy->getPrimaryDiscipline()->getSlug(),
]
);
$relatedVacanciesCount = $this->parentEntityManager->getRepository(Vacancy::class)
->countByOptionValue($vacancy->getPrimaryDiscipline()->getOption(), $vacancy->getPrimaryDiscipline(), $this->siteService->getSite());
} else {
$siteRelationOverviewOption = $this->config->get('site_vacancy_related_overview_option');
if ($siteRelationOverviewOption) {
foreach ($vacancy->getOptionValues()->toArray() as $optionValue) {
if ($optionValue->getOption()->getId() === $siteRelationOverviewOption->getId()) {
$mainOption = $optionValue->getOption();
$mainOptionValue = $optionValue;
break;
}
}
}
if (!$mainOption || !$mainOptionValue) {
return null;
}
$relatedOverviewPath = $this->router->generate('vacancies_with_filter', [
'filters' => $mainOption->getSlug().'/'.$mainOptionValue->getSlug(),
]);
$relatedVacanciesCount = $this->entityManager->getRepository(Vacancy::class)
->countByOptionValue($mainOption, $mainOptionValue, $this->siteService->getSite());
}
return [
'path' => $relatedOverviewPath,
'count' => $relatedVacanciesCount,
];
}
public function getSeoSnippetsFromFacets(array $facets, ?Site $site = null): array
{
$searchOptionValues = [];
foreach ($facets as $facet) {
foreach ($facet['values'] as $value) {
if (!$value['selected']) {
continue;
}
$searchOptionValues[] = $value['id'];
}
}
if (!$searchOptionValues) {
return [];
}
return $this->createSeoSnippetArray($searchOptionValues, $site);
}
public function getSeoSnippetsFromFilters(array $filters, ?Site $site): array
{
$searchOptionValues = [];
foreach ($filters as $filter) {
foreach ($filter as $subFilter) {
$searchOptionValues[] = $subFilter['id'];
}
}
if (!$searchOptionValues) {
return [];
}
return $this->createSeoSnippetArray($searchOptionValues, $site);
}
private function createSeoSnippetArray(array $searchOptionValues, ?Site $site): array
{
$arrayVacancyOverviewTitle = $this->config->get('site_vacancy_overview_title');
$overviewImage = $this->config->get('theme_vacancy_overview_image');
$locale = $this->request->getLocale() ?: $this->request->getDefaultLocale();
$allSeoSnippets = $this->getEntityManager()->getRepository(SeoSnippet::class)
->findByOptionValues($searchOptionValues, $site, false);
$seoSnippets = array_filter(
$allSeoSnippets,
static function (SeoSnippet $seoSnippet) use ($searchOptionValues) {
if (\count($searchOptionValues) !== $seoSnippet->getOptionValues()->count()) {
return false;
}
foreach ($seoSnippet->getOptionValues() as $option) {
if (!\in_array($option->getId(), $searchOptionValues, true)) {
return false;
}
}
return true;
}
);
$array = [
'id' => '',
'name' => '',
'snippet' => '',
'optionValues' => '',
'site' => '',
'headerImage' => $overviewImage,
'title' => $arrayVacancyOverviewTitle[$locale] ?? '',
'ogImage' => '',
'metaTitle' => '',
'metaDescription' => '',
];
$seoSnippet = [] !== $seoSnippets ? reset($seoSnippets) : $array;
if (\is_array($seoSnippet)) {
return $seoSnippet;
}
$fallBackTitle = $arrayVacancyOverviewTitle[$locale] ?? '';
$array = [
'id' => $seoSnippet->getId(),
'name' => $seoSnippet->getName(),
'snippet' => $seoSnippet->getSnippet(),
'optionValues' => $seoSnippet->getOptionValues(),
'site' => $seoSnippet->getSite(),
'headerImage' => $seoSnippet->getHeaderImage() ?? $overviewImage,
'title' => $seoSnippet->getTitle() ? $seoSnippet->getTitle() : $fallBackTitle,
'ogImage' => $seoSnippet->getOgImage(),
'metaTitle' => $seoSnippet->getMetaTitle(),
'metaDescription' => $seoSnippet->getMetaDescription(),
];
return $array;
}
/**
* @return Asset|string|null
*/
public function getOgImage(Vacancy $vacancy)
{
if ($vacancy->getOgImage() instanceof OgImage) {
return $this->seo->renderOgImage($vacancy->getOgImage(), $vacancy);
}
$ogImageOptionValue = $this->getOgImageOptionValue($vacancy);
switch ($this->config->get('site_vacancy_detail_og_image_strategy')) {
case VacancySettingType::VACANCY_DETAIL_OG_IMAGE_STRATEGY_COMPANY_LOGO:
if ($vacancy->getCompany() && $vacancy->getCompany()->getLogo()) {
return $vacancy->getCompany()
->getLogo();
}
break;
case VacancySettingType::VACANCY_DETAIL_OG_IMAGE_STRATEGY_VACANCY_IMAGE:
if ($ogImageOptionValue) {
return $ogImageOptionValue->getVacancyImage();
}
break;
case VacancySettingType::VACANCY_DETAIL_OG_IMAGE_STRATEGY_VACANCY_HERO_IMAGE:
if ($ogImageOptionValue) {
return $ogImageOptionValue->getVacancyHeroImage();
}
break;
default:
if ($this->vacancyImage->getHeroImage($vacancy)) {
return $this->vacancyImage->getHeroImage($vacancy);
}
break;
}
if ($vacancy->getOgImage() instanceof OgImage) {
return $this->seo->renderOgImage($vacancy->getOgImage(), $vacancy);
}
return null;
}
protected function getOgImageOptionValue(Vacancy $vacancy): ?OptionValue
{
$ogImageOption = $this->config->get('site_vacancy_detail_og_image_option');
if (!$ogImageOption) {
return null;
}
// Determine the first optionValue with $ogImageOption as parent
foreach ($vacancy->getOptionValues() as $optionValue) {
if ($optionValue->getOption()->getId() === $ogImageOption->getId()) {
return $optionValue;
}
}
return null;
}
public function transformVacancy(Vacancy $vacancy, ?Site $site = null, bool $shouldFilterOptions = true): array
{
$optionValueRepository = $this->entityManager->getRepository(OptionValue::class);
$company = $vacancy->getCompany();
$recruiter = $vacancy->getRecruiter();
$optionValues = [];
/** @var OptionValue $optionValue */
foreach ($vacancy->getOptionValues() as $optionValue) {
$option = $optionValue->getOption();
if ($option->getSites()->count() && $shouldFilterOptions) {
if (!$option->getSites()->contains($site)) {
continue;
}
}
$optionValues[] = [
'id' => $optionValue->getId(),
'value' => $optionValue->getValue(),
'title' => $optionValue->getTitle(),
'sub_title' => $optionValue->getSubTitle(),
'slug' => $optionValue->getSlug(),
'vacancies' => [],
'external_reference' => $optionValue->getExternalReference(),
'vacancy_count' => $optionValueRepository->getVacancyCountByOptionValue($optionValue),
'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' => [],
],
'font_awesome_icon' => $optionValue->getFontAwesomeIcon(),
'content' => $optionValue->getContent(),
];
}
$primaryDiscipline = null;
if ($vacancy->getPrimaryDiscipline()) {
$primaryDiscipline = [
'id' => $vacancy->getPrimaryDiscipline()->getId(),
'value' => $vacancy->getPrimaryDiscipline()->getValue(),
'slug' => $vacancy->getPrimaryDiscipline()->getSlug(),
];
}
$secundaryDiscipline = null;
if ($vacancy->getSecundaryDiscipline()) {
$secundaryDiscipline = [
'id' => $vacancy->getSecundaryDiscipline()->getId(),
'value' => $vacancy->getSecundaryDiscipline()->getValue(),
'slug' => $vacancy->getSecundaryDiscipline()->getSlug(),
];
}
$site = null;
if ($vacancy->getSite()) {
$site = $this->transformSite($vacancy->getSite());
}
$sitesToFindOn = [];
if ($vacancy->getSitesToFindOn()) {
foreach ($vacancy->getSitesToFindOn() as $siteToFindOn) {
$sitesToFindOn[] = $this->transformSite($siteToFindOn);
}
}
$objectMultiMedia = [];
if ($this->featureManager->isActive(FeatureFlagListener::FEATURE_MULTI_MEDIA)) {
$objectMultiMedia = $this->multiMediaManager->getFeaturedObjectMultiMediaByClass(Vacancy::class);
}
$featuredMultiMedia = $this->getFeaturedMultiMedia($objectMultiMedia, $vacancy->getId());
$detailUrl = null;
if ($vacancy->getSlug()) {
$detailUrl = $this->siteService->generateRoute(
'vacancy_detail',
$vacancy->getSite(),
['id' => $vacancy->getId(), 'slug' => $vacancy->getSlug()]
);
}
/** @var Tag[] $labels */
$labels = $vacancy->getLabels()->toArray();
$data = [
'id' => $vacancy->getId(),
'external_id' => $vacancy->getExternalId(),
'external_reference' => $vacancy->getExternalReference(),
'created' => $vacancy->getCreated()?->format(\DATE_RFC3339),
'updated' => $vacancy->getUpdated()?->format(\DATE_RFC3339),
'contact_person' => $vacancy->getContactPerson(),
'contact_person_phone' => $vacancy->getContactPersonPhone(),
'slug' => $vacancy->getSlug(),
'title' => $vacancy->getTitle(),
'company_name' => $vacancy->getCompanyName(),
'primary_discipline' => $primaryDiscipline,
'secundary_discipline' => $secundaryDiscipline,
'site' => $site,
'sites_to_find_on' => $sitesToFindOn,
'city' => $vacancy->getCity(),
'zipcode' => $vacancy->getZipcode(),
'intro' => $vacancy->getIntro(),
'option_values' => $optionValues,
'main_icon' => [
'font_awesome_icon' => $vacancy->getMainIcon() ? $vacancy->getMainIcon()->getFontAwesomeIcon() : '',
],
'vacancy_usp_values' => [],
'usps' => [],
'featured' => $vacancy->isFeatured(),
'internal_vacancy' => $vacancy->isInternalVacancy(),
'start_date' => $vacancy->getStartDate()?->format(\DATE_RFC3339),
'job_start_date' => $vacancy->getJobStartDate()?->format(\DATE_RFC3339),
'new' => $vacancy->isNew(),
'knockout_questions' => $vacancy->getKnockoutQuestions(),
'address' => $vacancy->getAddress(),
'latitude' => $vacancy->getLatitude(),
'longitude' => $vacancy->getLongitude(),
'overview_image' => $vacancy->getOverviewImage() && $vacancy->getOverviewImage()->getFileName() ?
$this->assetUtil->getAssetUrlsForFilters($vacancy->getOverviewImage(), $this->imageSizes) :
null,
'hero' => $this->vacancyImage->getHeroImage($vacancy) && $this->vacancyImage->getHeroImage($vacancy)->getFileName() ?
$this->assetUtil->getAssetUrlsForFilters($this->vacancyImage->getHeroImage($vacancy), $this->imageSizes) :
null,
'detail_url' => $detailUrl,
'custom_salary' => $vacancy->getCustomSalary(),
'featured_multi_media' => $featuredMultiMedia,
'attribute_values' => $this->attributeManager->getNormalizedValues($vacancy),
'salary' => $vacancy->getSalary(),
'min_salary' => $vacancy->getMinSalary(),
'max_salary' => $vacancy->getMaxSalary(),
'salary_unit' => $vacancy->getSalaryUnit(),
'labels' => array_map(function (Tag $tag) {
return [
'value' => $tag->getValue(),
'appearance' => $tag->getAppearance(),
];
}, $labels),
];
if (!empty($vacancy->getExternalUrl())) {
$data['external_url'] = $vacancy->getExternalUrl();
}
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() && $company->getLogo()->getFileName()) ?
$this->assetUtil->getAssetUrlsForFilters($company->getLogo(), $this->imageSizes) : null,
'video' => $company->getVideo(),
'detail_page_published' => $company->isDetailPagePublished(),
];
}
if ($recruiter) {
$data['recruiter'] = [
'id' => $recruiter->getId(),
'first_name' => $recruiter->getFirstName(),
'last_name_prefix' => $recruiter->getLastNamePrefix(),
'last_name' => $recruiter->getLastName(),
'email' => $recruiter->getEmail(),
'phone' => $recruiter->getPhone(),
'linkedin_url' => $recruiter->getLinkedinUrl(),
'intro' => $recruiter->getIntro(),
'description' => $recruiter->getDescription(),
'picture_name' => $recruiter->getPictureName(),
'updated_at' => $recruiter->getUpdatedAt(),
'vacancies' => [],
'external_reference' => $recruiter->getExternalReference(),
'companies' => [],
];
}
return $data;
}
/**
* @throws NonUniqueResultException
*/
public function transformVacancies(array $vacancies, ?Site $site = null): array
{
$vacancyOutput = [];
/** @var Vacancy $vacancy */
foreach ($vacancies as $vacancy) {
$vacancyOutput[] = $this->transformVacancy($vacancy, $site);
}
return $vacancyOutput;
}
public function getVacanciesResponseByRequest(Request $request, bool $isMap = false): SearchResultSet
{
if ($isMap) {
return $this->getVacancyReader()->getMapSearchResultSetByRequest($request);
}
return $this->getVacancyReader()->getSearchResultSetByRequest($request);
}
public function getVacancyReader(): AbstractVacancyReader
{
if ($this->featureManager->isActive(FeatureFlagListener::FEATURE_GOOGLE_CLOUD_TALENT_SOLUTION)) {
return $this->readerCollection->get(GoogleCloudTalentSolutionReader::class);
}
if ($this->featureManager->isActive(FeatureFlagListener::FEATURE_CHILD_SITE)) {
return $this->readerCollection->get(MultiSiteReader::class);
}
if ($this->translatableListener instanceof TranslatableListener) {
return $this->readerCollection->get(MultilingualDoctrineReader::class);
}
return $this->readerCollection->get(DoctrineReader::class);
}
/**
* @return int
*/
public function getVacancyCount(Request $request): ?int
{
return $this->getVacancyReader()->getVacancyCount($request);
}
public function getRecentVacancies(Request $request): array
{
$vacancies = [];
$companyLogos = [];
if (!$ids = $request->get('vacancyIds')) {
return [];
}
return $this->getVacancyReader()->getVacanciesByIds($ids);
}
/**
* @return Vacancy[]
*/
public function getVacanciesByContext(QueryContext $queryContext): array
{
$locale = null;
// If no fallback, exclude the vacancies with no translations by getting vacancies from only current locale
if (false === $this->translatableListener?->getTranslationFallback()) {
$locale = $this->translatableListener?->getListenerLocale();
}
/** @var VacancyRepository $repository */
$repository = $this->getEntityManager()->getRepository(Vacancy::class);
return $repository->findVacanciesWithContext($queryContext, $locale);
}
public function getVacancyCountByContext(QueryContext $queryContext): int
{
$locale = null;
if (false === $this->translatableListener?->getTranslationFallback()) {
$locale = $this->translatableListener?->getListenerLocale();
}
/** @var VacancyRepository $repository */
$repository = $this->getEntityManager()->getRepository(Vacancy::class);
return $repository->findVacancyCountByContext($queryContext, $locale);
}
private function getCompanyLogos(array $responseData): array
{
$companyLogos = [];
foreach ($responseData as $vacancyData) {
if (!empty($vacancyData->company)
&& !empty($vacancyData->company->logo)
&& empty($companyLogos[$vacancyData->company->id])) {
$companyLogos[$vacancyData->company->id] = (array) $vacancyData->company->logo;
}
}
return $companyLogos;
}
public function isCacheAble(): bool
{
return $this->getVacancyReader()->isCacheAble();
}
public function addSitesToVacancy(Vacancy $vacancy, string $siteMappingValue): Vacancy
{
$vacancy->getSitesToFindOn()->clear();
if (!$this->sites) {
$this->sites = $this->entityManager->getRepository(Site::class)->findAll();
}
foreach ($this->sites as $site) {
if (\in_array($siteMappingValue, $site->getMappingValues() ?? [], true)) {
$vacancy->addSiteToFindOn($site);
}
}
return $vacancy;
}
public function fetchVacanciesWithApplicantsBelowThreshold(int $threshold, ?int $limit = null): array
{
$vacancies = $this->entityManager
->getRepository(Vacancy::class)
->findVacanciesWithApplicantCountLessThan($threshold, $limit)
;
return array_filter($vacancies, function (array $vacancy) {
if ($this->authorizationChecker->isGranted('ROLE_ADMIN_MANAGER')) {
return true;
}
if ($this->authorizationChecker->isGranted('ROLE_ADMIN_JOB_OWNER') && $vacancy['vacancy']->getCompany()) {
return $vacancy['vacancy']->getCompany()->getConsultant() ===
$this->tokenStorage->getToken()->getUser();
}
return false;
});
}
/**
* @throws Exception
*
* @return array
*/
public function fetchExpiringVacancies(int $days, ?int $limit = null)
{
$vacancies = $this->entityManager->getRepository(Vacancy::class)
->findExpiringInDays($days, $limit);
if ($this->featureManager->isActive(FeatureFlagListener::FEATURE_COMPANY_DASHBOARD)) {
$vacancies = array_filter($vacancies, function (Vacancy $vacancy) {
if ($this->authorizationChecker->isGranted('ROLE_ADMIN_MANAGER')) {
return true;
}
if ($this->authorizationChecker->isGranted('ROLE_ADMIN_JOB_OWNER')) {
return $vacancy->getCompany()->getConsultant() === $this->tokenStorage->getToken()->getUser();
}
return false;
});
}
return $vacancies;
}
public function getActiveVacanciesByOptionValue(OptionValue $optionValue): array
{
return $this->entityManager->getRepository(Vacancy::class)->findPublishedVacancies(null, $optionValue->getId(), 10);
}
public function getDescriptionFields(bool $mandatoryOnly = false): array
{
if (!$descriptionForm = $this->entityManager->getRepository(VacancyDescriptionForm::class)->findOneBy(['type' => 'regular'])) {
return [];
}
$descriptionFields = [];
$mandatoryFields = [];
$formFields = json_decode($descriptionForm->getFields(), true);
foreach ($formFields as $field) {
$descriptionFields[] = $field['name'];
if (!empty($field['required']) && true === $field['required']) {
$mandatoryFields[] = $field['name'];
}
}
if ($mandatoryOnly) {
return $mandatoryFields;
}
return $descriptionFields;
}
private function transformSite(Site $site): array
{
return [
'id' => $site->getId(),
'name' => $site->getName(),
];
}
public function getPublishedVacancies(
?int $limit = null,
?int $companyId = null,
?int $filterId = null,
?Site $site = null,
array $context = []
): array {
return $this->getEntityManager()->getRepository(Vacancy::class)
->findPublishedVacancies(
$companyId,
$filterId,
$limit,
null,
$site
)
;
}
public function getSitemapVacancies(): array
{
return $this->entityManager->getRepository(Vacancy::class)
->findSitemapVacancies()
;
}
private function getEntityManager(): EntityManagerInterface
{
if ($this->featureManager->isActive(FeatureFlagListener::FEATURE_CHILD_SITE)) {
return $this->parentEntityManager;
}
return $this->entityManager;
}
public function getFeaturedMultiMedia(array $objectMultiMedia, int $vacancyId): array
{
if (!$this->featureManager->isActive(FeatureFlagListener::FEATURE_MULTI_MEDIA)) {
return [];
}
$serializedFeaturedMultiMedia = [];
$relatedOjectMultiMedia = array_filter($objectMultiMedia, static function (ObjectMultiMedia $relatedOjectMultiMedia) use ($vacancyId) {
return $relatedOjectMultiMedia->getForeignKey() === $vacancyId;
});
$featuredObjectMultiMedia = array_shift($relatedOjectMultiMedia);
if ($featuredObjectMultiMedia) {
$featuredMultiMedia = $featuredObjectMultiMedia->getMultiMedia();
$serializedFeaturedMultiMedia = [
'id' => $featuredMultiMedia->getId(),
'adapter' => $featuredMultiMedia->getAdapter(),
'title' => $featuredMultiMedia->getTitle(),
'embed_url' => $featuredMultiMedia->getEmbedUrl(),
'external_thumbnail' => $featuredMultiMedia->getExternalThumbnail() ?? '',
'custom_thumbnail' => $featuredMultiMedia->getCustomThumbnail() ? $this->assetUtil->getAssetUrlsForFilters($featuredMultiMedia->getCustomThumbnail(), $this->imageSizes) : null,
];
}
return $serializedFeaturedMultiMedia;
}
public function getViewsPerVacancyData(string $startDate = '30daysAgo', string $endDate = '1daysAgo', string $company = 'All'): array
{
$options = ['startdate' => $startDate, 'enddate' => $endDate];
$viewData = $this->analyticsClient->getViewsPerVacancyData($options);
$viewData = $this->googleAnalyticsService->getVacanciesInformation($viewData, $company);
$companies = $this->entityManager->getRepository(Company::class)->getCompanyNames();
sort($companies);
array_unshift($companies, 'All');
return ['viewData' => $viewData, 'companies' => $companies, 'option' => $options, 'currentCompanySelected' => $company];
}
/**
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/
public function renderECommerceScript(Vacancy $vacancy): string
{
$category = null;
$categoryOption = $this->config->get('site_google_category_option');
if ($categoryOption) {
$category = $vacancy->getOptionValues()->filter(
function (OptionValue $optionValue) use ($categoryOption) {
return $optionValue->getOption()->getId() === $categoryOption->getId();
}
)->first()
;
}
$output = $this->twig->render('scripts/e_commerce_vacancy_detail_script.html.twig', [
'category' => $category ?? null,
'vacancy' => $vacancy,
]);
if ($this->featureManager->isActive(FeatureFlagListener::FEATURE_HIRESERVE)) {
$output .= $this->twig->render('scripts/ubeeo_ecommerce_vacancy_detail_script.html.twig', [
'ubeeo_website_id' => $this->parameterBag->get('site_hire_serve_web_site_id'),
'vacancy' => $vacancy,
]);
}
return $output;
}
/**
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/
public function renderHeadEndScriptForVacancyDetail(): string
{
if (
$this->parameterBag->get('site_ingoedebanen_enable_tracking') &&
$this->featureManager->isActive(FeatureFlagListener::FEATURE_INGOEDEBANEN_ADVANCED_STATISTICS)
) {
return $this->twig->render('scripts/ingoedebanen_start_script.html.twig');
}
return '';
}
/**
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/
public function renderBodyEndScriptForVacancyDetail(Vacancy $vacancy): string
{
if (
$this->parameterBag->get('site_ingoedebanen_enable_tracking') &&
$this->featureManager->isActive(FeatureFlagListener::FEATURE_INGOEDEBANEN_ADVANCED_STATISTICS)
) {
return $this->twig->render('scripts/ingoedebanen_vacancy_end_script.html.twig', [
'vacancy' => $vacancy,
]);
}
return '';
}
public function getCompanyVacancies(int $companyId): array
{
return $this->getEntityManager()->getRepository(Vacancy::class)
->findCompanyVacancies($companyId);
}
/**
* @return array<int|string, array<int|string, mixed>>
*/
public function getVacancyUspValues(array $vacancy, array $vacancyUspCollection, ?Option $vacancyUspStrategyOption): array
{
$vacancyUspCollection = ArrayUtil::setKeysToSubArrayValue('id', $vacancyUspCollection);
$vacancyUspValues = ArrayUtil::getSubArrayOrEmptyArray('vacancyUspValues', $vacancy);
$optionValues = ArrayUtil::getSubArrayOrEmptyArray('optionValues', $vacancy);
if ($vacancyUspStrategyOption instanceof Option && \count($optionValues) > 0 && 0 === \count($vacancyUspValues)) {
foreach ($optionValues as $optionValue) {
if (($optionValue['option']['id'] ?? false) === $vacancyUspStrategyOption->getId()
&& \is_array($optionValue['vacancyUspValues'] ?? null)
) {
$vacancyUspValues = $optionValue['vacancyUspValues'];
break;
}
}
}
$output = [];
if (0 === \count($vacancyUspValues)) {
return $output;
}
foreach ($vacancyUspValues as $vacancyUspValue) {
if (!\is_array($vacancyUspValue)) {
continue;
}
$vacancyUspValueId = $vacancyUspValue['id'] ?? null;
if (!\is_string($vacancyUspValueId) && !\is_int($vacancyUspValueId)) {
continue;
}
$foundVacancyUspValue = $vacancyUspCollection[$vacancyUspValueId] ?? null;
if (!\is_array($foundVacancyUspValue)) {
continue;
}
$vacancyUsp = $foundVacancyUspValue['vacancyUsp'] ?? null;
$vacancyUspId = $foundVacancyUspValue['vacancyUsp']['id'] ?? null;
if (\is_array($vacancyUsp)
&& (\is_int($vacancyUspId) || \is_string($vacancyUspId))
) {
if (!\array_key_exists($vacancyUspId, $output)) {
$title = $vacancyUsp['title'] ?? null;
$position = $vacancyUsp['position'] ?? null;
$output[$vacancyUspId] = [
'title' => \is_string($title) ? $title : 'Unavailable',
'position' => \is_string($position) ? $position : 1000,
'values' => [],
];
}
if (\array_key_exists('value', $foundVacancyUspValue)) {
$position = $foundVacancyUspValue['position'] ?? 1000;
if (!\is_array($output[$vacancyUspId]['values'] ?? null)) {
$output[$vacancyUspId]['values'] = [];
}
while (\array_key_exists($position, $output[$vacancyUspId]['values'])) {
++$position;
}
$output[$vacancyUspId]['values'][$position] = $foundVacancyUspValue['value'];
}
}
}
usort($output, fn (array $a, array $b) => $a['position'] <=> $b['position']);
return ArrayUtil::ksortSubArraysByKeys('values', $output);
}
}