<?php
namespace App\Renderer;
use App\Component\Attributes\AttributeManager;
use App\Component\Attributes\AttributeManagerFactory;
use App\Component\Configuration\Util\Config;
use App\Component\FeedbackCompany\Manager\FeedbackCompanyManager;
use App\Decorator\VacancyDecorator;
use App\Decorator\VacancyOptionDecorator;
use App\Entity\ApplicationProcessItemCollection;
use App\Entity\Asset;
use App\Entity\CompanyAttribute;
use App\Entity\Gallery;
use App\Entity\Option;
use App\Entity\Recruiter;
use App\Entity\RecruiterAttribute;
use App\Entity\Route as RouteEntity;
use App\Entity\Testimonial;
use App\Entity\Vacancy;
use App\Entity\VacancyAttribute;
use App\Entity\VacancyDescriptionForm;
use App\EventListener\FeatureFlagListener;
use App\Form\Setting\VacancySettingType;
use App\Manager\EventManager;
use App\Manager\GoogleForJobsManager;
use App\Manager\TagManager;
use App\Model\Tealium\Base;
use App\Model\Vacancy\VacancyWrapper;
use App\Service\UtmService;
use App\Service\VacancyService;
use App\Templating\Decorator;
use App\Util\AssetUtil;
use App\Util\VacancyFallback;
use App\Util\VacancyImage;
use App\Util\VacancyUspValueUtil;
use App\Util\VacancyUtil;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Flagception\Manager\FeatureManagerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
class VacancyRenderer
{
public const RELATED_VACANCIES_LIMIT = 4;
protected ParameterBagInterface $parameterBag;
protected VacancyDecorator $vacancyDecorator;
protected VacancyOptionDecorator $vacancyOptionDecorator;
protected FeatureManagerInterface $featureManager;
protected VacancyService $vacancyService;
protected TagManager $tagManager;
protected VacancyImage $vacancyImage;
protected Environment $twig;
protected Decorator $templateDecorator;
protected Page $pageRenderer;
protected RouterInterface $router;
protected EntityManagerInterface $entityManager;
protected RequestStack $requestStack;
protected EventManager $eventManager;
protected AttributeManager $vacancyAttributeManager;
protected AttributeManager $recruiterAttributeManager;
private Config $config;
private GoogleForJobsManager $googleForJobsManager;
private AssetUtil $assetUtil;
protected AttributeManager $companyAttributeManager;
private FeedbackCompanyManager $feedbackCompanyManager;
/**
* VacancyRenderer constructor.
*/
public function __construct(
ParameterBagInterface $parameterBag,
VacancyDecorator $vacancyDecorator,
VacancyOptionDecorator $vacancyOptionDecorator,
FeatureManagerInterface $featureManager,
VacancyService $vacancyService,
TagManager $tagManager,
VacancyImage $vacancyImage,
Environment $twig,
Decorator $templateDecorator,
Page $pageRenderer,
RouterInterface $router,
EntityManagerInterface $entityManager,
RequestStack $requestStack,
GoogleForJobsManager $googleForJobsManager,
Config $config,
AssetUtil $assetUtil,
FeedbackCompanyManager $feedbackCompanyManager,
EventManager $eventManager,
AttributeManagerFactory $attributeManagerFactory,
private readonly VacancyFallback $vacancyFallback,
) {
$this->parameterBag = $parameterBag;
$this->vacancyDecorator = $vacancyDecorator;
$this->vacancyOptionDecorator = $vacancyOptionDecorator;
$this->featureManager = $featureManager;
$this->vacancyService = $vacancyService;
$this->tagManager = $tagManager;
$this->vacancyImage = $vacancyImage;
$this->twig = $twig;
$this->templateDecorator = $templateDecorator;
$this->pageRenderer = $pageRenderer;
$this->router = $router;
$this->entityManager = $entityManager;
$this->requestStack = $requestStack;
$this->googleForJobsManager = $googleForJobsManager;
$this->config = $config;
$this->assetUtil = $assetUtil;
$this->feedbackCompanyManager = $feedbackCompanyManager;
$this->eventManager = $eventManager;
$this->vacancyAttributeManager = $attributeManagerFactory->create(VacancyAttribute::class);
$this->companyAttributeManager = $attributeManagerFactory->create(CompanyAttribute::class);
$this->recruiterAttributeManager = $attributeManagerFactory->create(RecruiterAttribute::class);
}
/**
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/
public function renderDetail(
Request $request,
VacancyWrapper $vacancyWrapper,
string $template = '@default/pages/vacancy_detail.html.twig'
): Response {
$vacancy = $vacancyWrapper->vacancy;
// TODO: before enabling, do not make multiple requests to same resource
$overviewImage = $vacancyWrapper->vacancyImages->overviewImage;
$companyLogo = $vacancyWrapper->vacancyImages->companyLogo;
if ((!$vacancy || $vacancy->isDeleted()) && $redirect = $this->redirectVacancyByStrategy($vacancy)) {
return $redirect;
}
$relatedVacancies = [];
$relatedOverview = null;
if ($this->featureManager->isActive(FeatureFlagListener::FEATURE_VACANCY_RELATED_VACANCIES)) {
$relatedVacancies = $this->vacancyService->getRelatedVacancies($request, $vacancy, self::RELATED_VACANCIES_LIMIT);
$relatedOverview = $this->vacancyService->getRelatedOverviewMeta($vacancy);
}
$vacancyArray = [$vacancy];
$this->vacancyDecorator->decorate($relatedVacancies);
$this->vacancyDecorator->decorate($vacancyArray, true);
$this->vacancyOptionDecorator->decorate($vacancyArray);
$this->vacancyOptionDecorator->decorate($relatedVacancies);
/** @var Vacancy $vacancy */
$vacancy = reset($vacancyArray);
// detach entity because of mutation
$this->entityManager->detach($vacancy);
if (!$vacancy->getRecruiter()) {
$recruiterStrategy = $this->config->get('site_vacancy_detail_recruiter_strategy');
switch ($recruiterStrategy) {
case VacancySettingType::VACANCY_DETAIL_RECRUITER_STRATEGY_OPTION:
$recruiterOption = $this->config->get('site_vacancy_detail_recruiter_option');
if ($recruiterOption) {
$recruiterOptionOptionValues = VacancyUtil::getOptionValuesByOptionId($vacancy, $recruiterOption->getId());
$recruiterOptionOptionValue = $recruiterOptionOptionValues->first();
if ($recruiterOptionOptionValue) {
$vacancy->setRecruiter($recruiterOptionOptionValue->getRecruiter());
}
}
break;
case VacancySettingType::VACANCY_DETAIL_RECRUITER_STRATEGY_FALLBACK:
$recruiterFallback = $this->config->get('site_vacancy_detail_recruiter_fallback');
if ($recruiterFallback) {
$recruiter = $this->entityManager->getRepository(Recruiter::class)
->find($recruiterFallback->getId());
$vacancy->setRecruiter($recruiter);
}
break;
}
}
$recruiter = $vacancy->getRecruiter();
if ($recruiter instanceof Recruiter) {
$recruiter->setAttributes($this->recruiterAttributeManager->getNormalizedValues($recruiter));
$vacancy->setRecruiter($recruiter);
}
$this->calculateGallery($vacancy);
$ogImage = $this->vacancyService->getOgImage($vacancy);
$optionsForTemplate = [];
foreach ($vacancy->getOptionValues() as $optionValue) {
if (empty($optionsForTemplate[$optionValue->getOption()->getInternalName()])) {
$optionsForTemplate[$optionValue->getOption()->getInternalName()] = [];
}
$optionsForTemplate[$optionValue->getOption()->getInternalName()][] = $optionValue;
}
$relatedTestimonials = [];
if ($vacancy->getTags()) {
$relatedTestimonials = $this->tagManager->getEntitiesByRelatedTags($vacancy, Testimonial::class);
}
if ($vacancy->getCompany() && $vacancy->getCompany()->getDescription() && $description = json_decode($vacancy->getCompany()->getDescription(), true)) {
$vacancy->getCompany()->setDescription(implode('', $description));
}
if (VacancySettingType::VACANCY_DETAIL_ADDRESS_SIDEBLOCK_SOURCE_COMPANY === $this->config->get('site_vacancy_detail_address_sideblock_source')) {
$vacancy = $this->vacancyService->setCompanyAddress($vacancy);
}
$utm = UtmService::getUtmArrayFromRequest($this->requestStack->getMainRequest());
$applyUrl = $this->router->generate('vacancy_apply', array_merge(['id' => $vacancy->getId()], $utm));
$applicationProcess = null;
if (
$this->featureManager->isActive(FeatureFlagListener::FEATURE_APPLICATION_PROCESS) &&
!$applicationProcess = $vacancy->getApplicationProcess()
) {
$applicationProcess = $this->entityManager
->getRepository(ApplicationProcessItemCollection::class)
->findOneBy(['default' => true]);
}
$vacancy->options = $optionsForTemplate;
$route = $this->entityManager->getRepository(RouteEntity::class)
->findOneBy(['name' => $request->get('_route')])
;
$options = [];
if ($this->featureManager->isActive(FeatureFlagListener::FEATURE_TEALIUM)) {
$options['tealium_element'] = Base::createForVacancy($vacancy, $this->config);
}
$overviewImages = array_map(function (Asset $asset) {
return $this->assetUtil->getAssetUrlsForFilters($asset);
}, $this->vacancyImage->getOverviewImages($vacancy));
$heroImages = array_map(function (Asset $asset) {
return $this->assetUtil->getAssetUrlsForFilters($asset);
}, $this->vacancyImage->getHeroImages($vacancy));
if ($vacancy->getCompany()) {
$vacancy->getCompany()->setAttributes(
$this->companyAttributeManager->getNormalizedValues($vacancy->getCompany())
);
}
$vacancy->setAttributes($this->vacancyAttributeManager->getNormalizedValues($vacancy));
$reviews = [];
if (
$this->featureManager->isActive(FeatureFlagListener::FEATURE_FEEDBACK_COMPANY) &&
$vacancy->getCompany()
) {
$reviews = $this->feedbackCompanyManager->getReviewsForCompany($vacancy->getCompany());
}
/** @var Option|null $vacancyUspStrategyOption */
$vacancyUspStrategyOption = $this->config->get('site_vacancy_usp_strategy_option');
$vacancy->setCalendar($this->vacancyFallback->getCalendar($vacancy));
return $this->pageRenderer
->withEntity($vacancy)
->renderPage(
'{vacancy}',
$template,
[
'vacancy' => $vacancy,
'apiOverviewImage' => $overviewImage,
'apiCompanyLogo' => $companyLogo,
'apiDetailImage' => $vacancyWrapper->vacancyImages->detailImage,
'apiCompanyHero' => $vacancyWrapper->vacancyImages->companyHeroImage,
'jobPostingStructuredData' => $this->googleForJobsManager->generateStructureData($vacancy),
'detailImage' => $this->vacancyImage->getDetailImage($vacancy),
'overviewImage' => $this->vacancyImage->getOverviewImage($vacancy),
'overviewImages' => $overviewImages,
'heroImage' => $this->vacancyImage->getHeroImage($vacancy),
'heroImages' => $heroImages,
'relatedVacancies' => $relatedVacancies,
'relatedOverview' => $relatedOverview,
'vacancyUsps' => $vacancy->getUsps()->getValues(),
'vacancyUspValues' => VacancyUspValueUtil::getForVacancyDetail($vacancy, $vacancyUspStrategyOption),
'relatedTestimonials' => $relatedTestimonials,
'applyUrl' => $applyUrl,
'applicationProcess' => $applicationProcess,
'e_commerce_script' => $this->vacancyService->renderECommerceScript($vacancy),
'head_end_script' => $this->vacancyService->renderHeadEndScriptForVacancyDetail(),
'body_end_script' => $this->vacancyService->renderBodyEndScriptForVacancyDetail($vacancy),
'company_reviews' => $reviews,
'description' => $this->getVacancyDescription($vacancy),
'upcoming_event' => $this->featureManager->isActive(FeatureFlagListener::FEATURE_EVENTS) ?
$this->eventManager->getUpcomingEvent() : null,
],
[
$this->vacancyService->getVacancyCanonicalUrl($vacancy),
],
[
'metaTitle' => $vacancy->getMetaTitle(),
'metaDescription' => empty($vacancy->getMetaDescription()) ? $vacancy->getIntro() : $vacancy->getMetaDescription(),
'ogImage' => $ogImage,
],
$vacancy,
$route,
$vacancyWrapper->vacancyImages->heroImage->hasUrlForFilter('original') ?
$vacancyWrapper->vacancyImages->heroImage->getUrlForFilter('original') :
$this->vacancyImage->getHeroImage($vacancy),
$options
);
}
private function redirectVacancyByStrategy(?Vacancy $vacancy): ?Response
{
$deletedStrategy = $this->config->get('site_vacancy_deleted_vacancy_strategy');
$expirationPeriod = $this->config->get('site_vacancy_deleted_vacancy_expiration_period');
$defaultRedirect = '404_page';
$preferedRedirect = $this->config->get('site_vacancy_deleted_vacancy_redirect') ?: $defaultRedirect;
if (VacancySettingType::DELETED_STRATEGY_THROW_404 === $deletedStrategy) {
return new RedirectResponse(
$this->router->generate($defaultRedirect)
);
}
if (
null !== $vacancy &&
null !== $expirationPeriod &&
$expirationPeriod < (new DateTime())->diff($vacancy->getDeleted())->days
) {
return new RedirectResponse(
$this->router->generate($preferedRedirect)
);
}
return null;
}
private function getVacancyDescription(Vacancy $vacancy): array
{
$form = $this->entityManager->getRepository(VacancyDescriptionForm::class)->findOneBy([]);
if (!($form instanceof VacancyDescriptionForm)) {
return [];
}
$fields = json_decode($form->getFields(), true);
$namedFields = array_column($fields, 'name', 'label');
$vacancyDescription = json_decode($vacancy->getDescription(), true);
$description = [];
foreach ($namedFields as $key => $value) {
if (empty($body = $vacancyDescription[$value] ?? '')) {
continue;
}
$description[$value] = [
'title' => $key,
'body' => $body,
];
}
return $description;
}
public function calculateGallery(Vacancy $vacancy): void
{
if (!$vacancy->getGallery()) {
$galleryStrategy = $this->config->get('site_vacancy_detail_gallery_strategy');
switch ($galleryStrategy) {
case VacancySettingType::VACANCY_DETAIL_GALLERY_STRATEGY_NONE:
default:
break;
case VacancySettingType::VACANCY_DETAIL_GALLERY_STRATEGY_COMPANY:
$company = $vacancy->getCompany();
if ($company) {
$vacancy->setGallery($company->getGallery());
}
break;
case VacancySettingType::VACANCY_DETAIL_GALLERY_STRATEGY_OPTION:
$galleryOption = $this->config->get('site_vacancy_detail_gallery_option');
if ($galleryOption) {
$galleryOptionOptionValues = VacancyUtil::getOptionValuesByOptionId($vacancy, $galleryOption->getId());
$galleryOptionOptionValue = $galleryOptionOptionValues->first();
if ($galleryOptionOptionValue) {
$vacancy->setGallery($galleryOptionOptionValue->getGallery());
}
}
break;
case VacancySettingType::VACANCY_DETAIL_GALLERY_STRATEGY_FALLBACK:
$galleryFallback = $this->config->get('site_vacancy_detail_gallery_fallback');
if ($galleryFallback) {
$gallery = $this->entityManager->getRepository(Gallery::class)->find($galleryFallback->getId());
$vacancy->setGallery($gallery);
}
break;
}
}
}
}