<?php
declare(strict_types=1);
namespace App\Component\GoogleCloud\TalentSolution\EventListener;
use App\Component\GoogleCloud\TalentSolution\Client\AbstractClient;
use App\Component\GoogleCloud\TalentSolution\Client\CompanyClient;
use App\Component\GoogleCloud\TalentSolution\Client\VacancyClient;
use App\Component\GoogleCloud\TalentSolution\Handler\VacancyHandler;
use App\Entity\Company;
use App\Entity\Repository\VacancyRepository;
use App\Entity\Vacancy;
use App\Event\VacancyEvent;
use App\EventListener\FeatureFlagListener;
use App\Repository\CompanyRepository;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use Flagception\Manager\FeatureManagerInterface;
use Google\ApiCore\ApiException;
use Google\ApiCore\ValidationException;
use Google\Cloud\Talent\V4beta1\Job;
use Psr\Log\LoggerInterface;
use Symfony\Component\Stopwatch\Stopwatch;
class EventListener
{
public const GCTS_STOPWATCH_SYNC_EVENT = 'gcts_stopwatch_sync_event';
protected Stopwatch $stopWatch;
/**
* GoogleCloudTalentSolutionEventListener constructor.
*/
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly VacancyClient $vacancyClient,
private readonly CompanyClient $companyClient,
private readonly LoggerInterface $logger,
private readonly FeatureManagerInterface $featureManager,
private readonly VacancyHandler $vacancyHandler,
) {
$this->stopWatch = new Stopwatch();
}
public function onExternalIntergationImportCompleted(): void
{
if (!$this->featureManager->isActive(FeatureFlagListener::FEATURE_GOOGLE_CLOUD_TALENT_SOLUTION)) {
return;
}
if ('prod' !== $_ENV['APP_ENV']) {
$this->logger->info('Not in a prod environment, vacancies and companies will not be synchronized to Google');
return;
}
$this->stopWatch->start(self::GCTS_STOPWATCH_SYNC_EVENT);
$this->logger->info('Start sync');
// GCTS requires vacancies to have companies, syncing these is step 1
try {
$this->syncCompanies();
} catch (Exception $exception) {
$message = 'Fetching GCTS company ids failed, this is a reason to stop sync. Stopping sync...';
$this->logger->error(
$message,
[
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'duration' => $this->stopWatch->stop(self::GCTS_STOPWATCH_SYNC_EVENT)->getDuration().'ms',
]
);
return;
}
$this->vacancyHandler->updateVacancyNames();
// If syncCompanies has finished, sync vacancies
$this->syncVacancies();
$this->logger->info(
sprintf(
'Sync finished in %s milliseconds',
$this->stopWatch->stop(self::GCTS_STOPWATCH_SYNC_EVENT)->getDuration()
)
);
}
public function onVacancyPersisted(VacancyEvent $vacancyEvent): void
{
if (!$this->featureManager->isActive(FeatureFlagListener::FEATURE_GOOGLE_CLOUD_TALENT_SOLUTION)) {
return;
}
if ('prod' !== $_ENV['APP_ENV']) {
$this->logger->info('Not in a prod environment, vacancies and companies will not be synchronized to Google');
return;
}
$vacancy = $vacancyEvent->getVacancy();
if (!$vacancy->getCompany() instanceof Company) {
return;
}
try {
/* @var Job $gctsVacancy */
$this->companyClient->saveCompany($vacancy->getCompany());
$this->entityManager->flush();
$this->vacancyClient->saveJob($vacancy);
$this->entityManager->persist($vacancy);
$this->entityManager->flush();
$this->logger->info(
sprintf(
'Saved vacancy %s to GCTS after %s',
$vacancy->getId(),
\get_class($vacancyEvent)
)
);
} catch (Exception $exception) {
$this->logger->error(
sprintf(
'Failure saving vacancy %s to GCTS after %s',
$vacancy->getId(),
\get_class($vacancyEvent)
),
[
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
]
);
}
}
public function onVacancyRemoved(VacancyEvent $vacancyEvent): void
{
if (!$this->featureManager->isActive(FeatureFlagListener::FEATURE_GOOGLE_CLOUD_TALENT_SOLUTION)) {
return;
}
if ('prod' !== $_ENV['APP_ENV']) {
$this->logger->info('Not in a prod environment, vacancies and companies will not be synchronized to Google');
return;
}
$vacancy = $vacancyEvent->getVacancy();
try {
$this->vacancyClient->deleteJob($vacancy);
if ($state = $this->entityManager->getFilters()->isEnabled('softdeleteable')) {
$this->entityManager->getFilters()->disable('softdeleteable');
}
$vacancy->setGoogleCloudName(null);
$this->entityManager->persist($vacancy);
$this->entityManager->flush();
if ($state) {
$this->entityManager->getFilters()->enable('softdeleteable');
}
$this->logger->info(
sprintf(
'Deleted vacancy %s from GCTS after %s',
$vacancy->getId(),
\get_class($vacancyEvent)
)
);
} catch (Exception $exception) {
$this->logger->error(
sprintf(
'Failure deleting vacancy %s from GCTS after %s',
$vacancy->getId(),
\get_class($vacancyEvent)
),
[
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
]
);
}
}
private function getLogMessage(string $action, string $object, int $count): string
{
return sprintf('Google cloud %s %s %s', $action, $count, $object);
}
/**
* @throws ApiException
* @throws ValidationException
* @throws \JsonException
*/
private function syncCompanies(): void
{
$externalCompanyIds = $this->companyClient->getCompanyIds();
/** @var CompanyRepository $companyRepo */
$companyRepo = $this->entityManager->getRepository(Company::class);
$companyIds = $companyRepo->getCompanyIds();
$externalSerenaCompanyIds = array_map('intval', array_keys($externalCompanyIds));
$deleteableCompaniesIds = array_diff($externalSerenaCompanyIds, $companyIds);
$updateableCompaniesIds = array_intersect($externalSerenaCompanyIds, $companyIds);
$insertableCompaniesIds = array_diff($companyIds, $externalSerenaCompanyIds);
$this->logger->info(
$this->getLogMessage(
'Deleting',
'companies',
\count($deleteableCompaniesIds)
)
);
foreach ($deleteableCompaniesIds as $deleteableCompanyid) {
$this->companyClient->deleteCompany($externalCompanyIds[$deleteableCompanyid]);
}
$gctsCompanies = [];
$this->logger->info(
$this->getLogMessage(
'Updating',
'companies',
\count($updateableCompaniesIds)
)
);
$updateableCompanies = $companyRepo->findBy(['id' => $updateableCompaniesIds]);
foreach ($updateableCompanies as $company) {
if (!\is_string($company->getGoogleCloudName())) {
$company->setGoogleCloudName($externalCompanyIds[$company->getId()]);
}
$gctsCompany = $this->companyClient->saveCompany($company);
$gctsCompanies[$company->getId()] = $gctsCompany?->getName();
}
$this->logger->info(
$this->getLogMessage(
'Creating',
'companies',
\count($insertableCompaniesIds)
)
);
$insertableCompanies = $companyRepo->findBy(['id' => $insertableCompaniesIds]);
foreach ($insertableCompanies as $company) {
$gctsCompany = $this->companyClient->saveCompany($company);
$gctsCompanies[$company->getId()] = $gctsCompany?->getName();
}
$companyRepo->updateCompanyGCTSNames($gctsCompanies);
}
private function syncVacancies(): void
{
// Sync available vacancies
/** @var VacancyRepository $vacancyRepo */
$vacancyRepo = $this->entityManager->getRepository(Vacancy::class);
$externalIds = $this->vacancyClient->getExistingRequisitionIds();
$externalSerenaIds = array_map('intval', array_keys($externalIds));
$internalIds = $vacancyRepo->findVacancyIds();
$deleteableVacancies = array_diff($externalSerenaIds, $internalIds);
$this->logger->info(
sprintf('Deleting %s vacancies from google cloud', \count($deleteableVacancies))
);
foreach ($deleteableVacancies as $deleteableVacancyId) {
if (!\array_key_exists($deleteableVacancyId, $externalIds)) {
continue;
}
$this->vacancyClient->deleteByName($externalIds[$deleteableVacancyId]);
}
$updatableVacancies = array_intersect($externalSerenaIds, $internalIds);
$this->logger->info(
sprintf('Updating %s vacancies from google cloud', \count($updatableVacancies))
);
$vacancies = $vacancyRepo->findBy(['id' => $updatableVacancies]);
try {
$this->vacancyClient->batchOperation($vacancies, AbstractClient::UPDATE);
} catch (Exception $e) {
}
$createVacancies = array_diff($internalIds, $externalSerenaIds);
$this->logger->info(
sprintf('Creating %s vacancies from google cloud', \count($createVacancies))
);
$vacancies = $vacancyRepo->findBy(['id' => $createVacancies]);
$this->vacancyClient->batchOperation($vacancies, AbstractClient::CREATE);
}
}