122 lines
3.8 KiB
PHP
122 lines
3.8 KiB
PHP
<?php
|
|||
|
|
|
||
|
|
namespace App\Services\JobApi\Transformers;
|
||
|
|
|
||
|
|
use App\Services\JobApi\DTO\JobData;
|
||
|
|
use App\Services\JobApi\Exceptions\TransformationException;
|
||
|
|
use DateTime;
|
||
|
|
|
||
|
|
class RemotiveTransformer implements JobTransformer
|
||
|
|
{
|
||
|
|
public function transform(array $data): JobData
|
||
|
|
{
|
||
|
|
try {
|
||
|
|
return new JobData(
|
||
|
|
externalId: (string) $data['id'],
|
||
|
|
title: $data['title'] ?? 'Untitled',
|
||
|
|
company: $data['company_name'] ?? 'Unknown Company',
|
||
|
|
description: $this->sanitizeDescription($data['description'] ?? ''),
|
||
|
|
location: $this->parseLocation($data),
|
||
|
|
salaryMin: $this->parseSalary($data['salary'] ?? null, 'min'),
|
||
|
|
salaryMax: $this->parseSalary($data['salary'] ?? null, 'max'),
|
||
|
|
salaryCurrency: 'USD',
|
||
|
|
jobType: $this->mapJobType($data['job_type'] ?? null),
|
||
|
|
sourceUrl: $data['url'] ?? null,
|
||
|
|
applyUrl: $data['url'] ?? null,
|
||
|
|
postedAt: $this->parseDate($data['publication_date'] ?? null),
|
||
|
|
expiresAt: null,
|
||
|
|
remoteAllowed: true, // Remotive only has remote jobs
|
||
|
|
experienceLevel: $this->mapExperienceLevel($data['candidate_required_location'] ?? null),
|
||
|
|
category: $data['category'] ?? null,
|
||
|
|
rawData: $data,
|
||
|
|
);
|
||
|
|
} catch (\Throwable $e) {
|
||
|
|
throw new TransformationException(
|
||
|
|
'Remotive',
|
||
|
|
'Failed to transform Remotive job data: ' . $e->getMessage(),
|
||
|
|
['data' => $data, 'error' => $e->getMessage()]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public function transformMany(array $jobs): array
|
||
|
|
{
|
||
|
|
return array_map(fn($job) => $this->transform($job), $jobs);
|
||
|
|
}
|
||
|
|
|
||
|
|
private function sanitizeDescription(string $description): string
|
||
|
|
{
|
||
|
|
// Remove HTML tags and clean up whitespace
|
||
|
|
$clean = strip_tags($description);
|
||
|
|
$clean = html_entity_decode($clean, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||
|
|
$clean = preg_replace('/\s+/', ' ', $clean);
|
||
|
|
return trim($clean);
|
||
|
|
}
|
||
|
|
|
||
|
|
private function parseLocation(array $data): ?string
|
||
|
|
{
|
||
|
|
$location = $data['candidate_required_location'] ?? null;
|
||
|
|
|
||
|
|
if (empty($location)) {
|
||
|
|
return 'Remote';
|
||
|
|
}
|
||
|
|
|
||
|
|
return $location;
|
||
|
|
}
|
||
|
|
|
||
|
|
private function parseSalary(?string $salary, string $type): ?float
|
||
|
|
{
|
||
|
|
if (empty($salary)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Try to extract numbers from salary string
|
||
|
|
if (preg_match_all('/[\d,]+/', $salary, $matches)) {
|
||
|
|
$numbers = array_map(fn($n) => (float) str_replace(',', '', $n), $matches[0]);
|
||
|
|
|
||
|
|
if (count($numbers) >= 2) {
|
||
|
|
return $type === 'min' ? min($numbers) : max($numbers);
|
||
|
|
} elseif (count($numbers) === 1) {
|
||
|
|
return $numbers[0];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
private function parseDate(?string $date): ?DateTime
|
||
|
|
{
|
||
|
|
if (empty($date)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
return new DateTime($date);
|
||
|
|
} catch (\Exception $e) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private function mapJobType(?string $type): ?string
|
||
|
|
{
|
||
|
|
if (empty($type)) {
|
||
|
|
return 'full-time';
|
||
|
|
}
|
||
|
|
|
||
|
|
return match (strtolower($type)) {
|
||
|
|
'full_time', 'full-time' => 'full-time',
|
||
|
|
'part_time', 'part-time' => 'part-time',
|
||
|
|
'contract' => 'contract',
|
||
|
|
'freelance' => 'freelance',
|
||
|
|
default => 'full-time',
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
private function mapExperienceLevel(?string $location): ?string
|
||
|
|
{
|
||
|
|
// Remotive doesn't provide experience level directly
|
||
|
|
// This is a placeholder that could be enhanced with ML/NLP
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|