2026-04-03 00:23:01 +03:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Services\JobApi\Transformers;
|
|
|
|
|
|
|
|
|
|
use App\Services\JobApi\DTO\JobData;
|
|
|
|
|
use App\Services\JobApi\Exceptions\TransformationException;
|
|
|
|
|
use DateTime;
|
|
|
|
|
|
|
|
|
|
class AdzunaTransformer implements JobTransformer
|
|
|
|
|
{
|
|
|
|
|
public function transform(array $data): JobData
|
|
|
|
|
{
|
|
|
|
|
try {
|
|
|
|
|
return new JobData(
|
|
|
|
|
externalId: (string) $data['id'],
|
|
|
|
|
title: $data['title'] ?? 'Untitled',
|
|
|
|
|
company: $data['company']['display_name'] ?? 'Unknown Company',
|
|
|
|
|
description: $this->sanitizeDescription($data['description'] ?? ''),
|
|
|
|
|
location: $this->parseLocation($data),
|
|
|
|
|
salaryMin: $data['salary_min'] ?? null,
|
|
|
|
|
salaryMax: $data['salary_max'] ?? null,
|
|
|
|
|
salaryCurrency: 'USD',
|
|
|
|
|
jobType: $this->mapJobType($data['contract_type'] ?? null),
|
|
|
|
|
sourceUrl: $data['redirect_url'] ?? null,
|
|
|
|
|
applyUrl: $data['redirect_url'] ?? null,
|
|
|
|
|
postedAt: $this->parseDate($data['created'] ?? null),
|
|
|
|
|
expiresAt: null,
|
|
|
|
|
remoteAllowed: $this->isRemote($data),
|
|
|
|
|
experienceLevel: null,
|
|
|
|
|
category: $data['category']['label'] ?? null,
|
|
|
|
|
rawData: $data,
|
|
|
|
|
);
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
throw new TransformationException(
|
|
|
|
|
'Adzuna',
|
|
|
|
|
'Failed to transform Adzuna 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
|
|
|
|
|
{
|
2026-04-04 18:19:34 +03:00
|
|
|
// Adzuna returns truncated descriptions with ellipsis (...)
|
|
|
|
|
// Keep the description as-is but strip excessive HTML while preserving basic formatting
|
|
|
|
|
$clean = strip_tags($description, '<p><br><ul><ol><li><strong><b><em><i><h1><h2><h3><h4>');
|
2026-04-03 00:23:01 +03:00
|
|
|
$clean = html_entity_decode($clean, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
2026-04-04 18:19:34 +03:00
|
|
|
|
|
|
|
|
// Convert multiple spaces to single space but preserve newlines
|
|
|
|
|
$clean = preg_replace('/[^\S\n]+/', ' ', $clean);
|
|
|
|
|
|
|
|
|
|
// Note: Adzuna API only provides truncated descriptions in search results.
|
|
|
|
|
// Full descriptions would require individual job detail API calls.
|
2026-04-03 00:23:01 +03:00
|
|
|
return trim($clean);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function parseLocation(array $data): ?string
|
|
|
|
|
{
|
|
|
|
|
$location = $data['location']['display_name'] ?? null;
|
|
|
|
|
|
|
|
|
|
if (empty($location)) {
|
|
|
|
|
// Fallback to area
|
|
|
|
|
$area = $data['location']['area'] ?? [];
|
|
|
|
|
if (!empty($area)) {
|
|
|
|
|
return implode(', ', $area);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $location;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function isRemote(array $data): bool
|
|
|
|
|
{
|
|
|
|
|
$title = strtolower($data['title'] ?? '');
|
|
|
|
|
$description = strtolower($data['description'] ?? '');
|
|
|
|
|
$location = strtolower($data['location']['display_name'] ?? '');
|
|
|
|
|
|
|
|
|
|
return str_contains($title, 'remote') ||
|
|
|
|
|
str_contains($description, 'remote') ||
|
|
|
|
|
str_contains($location, 'remote');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return match (strtolower($type)) {
|
|
|
|
|
'permanent', 'full_time' => 'full-time',
|
|
|
|
|
'part_time' => 'part-time',
|
|
|
|
|
'contract' => 'contract',
|
|
|
|
|
'temporary' => 'temporary',
|
|
|
|
|
default => 'full-time',
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|