<?php
namespace App\Manager;
use App\Annotations\JobPositionMappingProperty\Date;
use App\Annotations\JobPositionMappingProperty\Image;
use App\Annotations\JobPositionMappingProperty\Text;
use App\Component\Configuration\Util\Config;
use App\Entity\Asset;
use App\Entity\OptionValue;
use App\Entity\Vacancy;
use App\Transformer\VacancyMappingToJobPostingStructuredDataTransformer;
use App\Twig\DescriptionExtension;
use App\Util\AssetUtil;
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\ORM\EntityManagerInterface;
use Liip\ImagineBundle\Imagine\Cache\CacheManager;
use Money\Money;
use ReflectionClass;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Vich\UploaderBundle\Templating\Helper\UploaderHelper;
class GoogleForJobsManager
{
public const CONFIG_NAME = 'job_position_mapping';
private ParameterBagInterface $parameterBag;
private DescriptionExtension $descriptionExtension;
private VacancyMappingToJobPostingStructuredDataTransformer $dataTransformer;
private OptionManager $optionManager;
private UploaderHelper $uploaderHelper;
private CacheManager $cacheManager;
private Config $config;
private EntityManagerInterface $entityManager;
private AssetUtil $assetUtil;
/**
* GoogleForJobsManager constructor.
*/
public function __construct(
ParameterBagInterface $parameterBag,
DescriptionExtension $descriptionExtension,
VacancyMappingToJobPostingStructuredDataTransformer $dataTransformer,
OptionManager $optionManager,
UploaderHelper $uploaderHelper,
CacheManager $cacheManager,
Config $config,
EntityManagerInterface $entityManager,
AssetUtil $assetUtil
) {
$this->parameterBag = $parameterBag;
$this->descriptionExtension = $descriptionExtension;
$this->dataTransformer = $dataTransformer;
$this->optionManager = $optionManager;
$this->uploaderHelper = $uploaderHelper;
$this->cacheManager = $cacheManager;
$this->config = $config;
$this->entityManager = $entityManager;
$this->assetUtil = $assetUtil;
}
public function getVacancyMappingProperties(): array
{
$propertyMappings = [];
$jobPositionMappingProperties = [Text::class => 'text', Date::class => 'date', Image::class => 'image'];
$reader = new AnnotationReader();
$reflectionClass = new ReflectionClass(Vacancy::class);
foreach ($reflectionClass->getProperties() as $property) {
$propertyAnnotations = array_map(
static fn ($object): string => \get_class($object),
$reader->getPropertyAnnotations($property)
);
foreach (array_intersect($propertyAnnotations, array_keys($jobPositionMappingProperties)) as $mapping) {
$propertyName = $property->name;
if (Text::class === $mapping) {
$propertyName = 'vacancy.'.$property->name;
}
$propertyMappings[$jobPositionMappingProperties[$mapping]][] = $propertyName;
}
}
// add custom types
$propertyMappings['text'][] = 'companyName';
$propertyMappings['text'][] = 'salary';
$propertyMappings['image'][] = 'siteLogo';
// add options as placeholders
$options = array_map(
static fn ($option): array => ['id' => $option['id'], 'value' => sprintf('option.%s', $option['title'])],
$this->optionManager->getOptionsWithTitle()
);
$propertyMappings['text'] = array_merge($propertyMappings['text'], $options);
return $propertyMappings;
}
private function getMapping(): array
{
if (!$this->parameterBag->has(self::CONFIG_NAME)) {
return [];
}
return json_decode($this->parameterBag->get(self::CONFIG_NAME), true);
}
public function generateStructureData(Vacancy $vacancy): string
{
$propertyMappings = [];
$propertyAccessor = PropertyAccess::createPropertyAccessor();
// transform propertyMappings to single dimension with mapping as index
foreach ($this->getVacancyMappingProperties() as $type => $values) {
foreach ($values as $value) {
if (\is_array($value) && \array_key_exists('value', $value)) {
$value = $value['value'];
}
$propertyMappings[$value] = $type;
}
}
$mapping = $this->getMapping();
if ($vacancy->getGoogleForJobMapping()) {
$mapping = array_merge($mapping, json_decode($vacancy->getGoogleForJobMapping(), true));
}
foreach ($mapping as $placeholder => &$value) {
// replace nbsp to space
$value = str_replace("\xc2\xa0", ' ', $value);
$value = preg_replace_callback('/(\[\[(\S*)]])+/x', function ($matches) use (
$vacancy,
$propertyAccessor,
$placeholder
) {
$jsonData = json_decode($matches[2], true);
if (!\is_array($jsonData) || !\array_key_exists('value', $jsonData)) {
return '';
}
$matchValue = $jsonData['value'];
// description needs to be transformed
if ('vacancy.description' === $matchValue) {
return $this->descriptionExtension->getDescription($vacancy);
}
// description needs to be transformed
if ('companyName' === $matchValue) {
return $this->config->get('site_vacancy_job_posting_show_company_name') && $vacancy->getCompany() ?
$vacancy->getCompany()->getName() : $this->parameterBag->get('site_company_name');
}
// if baseSalary is set to special salary value (default), then return a json array when
// min salary is not zero
if ('baseSalary' === $placeholder && 'salary' === $matchValue) {
if ($vacancy->getMinSalary()->isZero()) {
return $vacancy->getSalaryAmount() > 0 ? ($vacancy->getSalaryAmount() / 100) : 0;
}
return json_encode([
'minValue' => ($vacancy->getMinSalary()->getAmount() / 100),
'maxValue' => ($vacancy->getMaxSalary()->greaterThan(
new Money(0, $vacancy->getMaxSalary()->getCurrency()))
? ($vacancy->getMaxSalary()->getAmount() / 100)
: 0
),
]);
}
if (0 === mb_strpos($matchValue, 'vacancy.')) {
$matchValue = str_replace('vacancy.', '', $matchValue);
if ($propertyAccessor->isReadable($vacancy, $matchValue)) {
return $propertyAccessor->getValue($vacancy, $matchValue);
}
return '';
}
if (0 === mb_strpos($matchValue, 'option.')) {
if (!\array_key_exists('id', $jsonData) || !$vacancy->getOptionValues()) {
return '';
}
$optionValues = $vacancy->getOptionValues()
->filter(fn (OptionValue $optionValue): bool => $optionValue->getOption() && $optionValue->getOption()->getId() === $jsonData['id']
)
->map(fn (OptionValue $optionValue): ?string => $optionValue->getValue())
->toArray()
;
return implode(',', $optionValues);
}
return '';
}, $value);
if ('directApply' === $placeholder) {
$value = !($vacancy->getExternalUrl() || $vacancy->getExternalDetailUrl());
continue;
}
/* @var string $value */
if (!\array_key_exists($value, $propertyMappings)) {
continue;
}
if ('date' === $propertyMappings[$value] && $propertyAccessor->isReadable($vacancy, $value)) {
$dateTime = $propertyAccessor->getValue($vacancy, $value);
if ($dateTime instanceof \DateTime) {
$value = $dateTime->format(\DATE_ATOM);
} else {
$value = '';
}
} elseif ('image' === $propertyMappings[$value] && $propertyAccessor->isReadable($vacancy, $value)) {
$image = $propertyAccessor->getValue($vacancy, $value);
if (!$image || Asset::class !== $this->entityManager->getClassMetadata(\get_class($image))->getName()) {
continue;
}
if (!$relativePath = $this->uploaderHelper->asset($image, 'assetFile')) {
continue;
}
$value = $this->cacheManager->getBrowserPath($relativePath, 'square_100x100');
} elseif ('siteLogo' === $value) {
/**
* @var asset $image
*/
$image = $this->config->get('site_logo');
$value = $this->assetUtil->getAssetUrlForFilter($image, 'square_100x100');
}
}
unset($value);
$structuredData = $this->dataTransformer->transform($mapping);
return $structuredData ?? '';
}
}