Files
hr/app/Services/Import/ImportService.php
2026-07-30 17:24:40 +05:00

220 lines
6.9 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Import;
use App\DTOs\Import\BonusRowDto;
use App\DTOs\Import\EmployeeRowDto;
use App\DTOs\Import\ImportResultDto;
use App\DTOs\Import\ReportRowDto;
use App\DTOs\Import\VacationRowDto;
use App\Enums\ImportType;
use App\Models\Bonus;
use App\Models\DisciplinaryReport;
use App\Models\Employee;
use App\Models\Vacation;
use App\Services\Employee\EmployeeService;
use PhpOffice\PhpSpreadsheet\IOFactory;
class ImportService
{
public function __construct(
private readonly EmployeeImportTransformer $employeeTransformer,
private readonly VacationImportTransformer $vacationTransformer,
private readonly BonusImportTransformer $bonusTransformer,
private readonly ReportImportTransformer $reportTransformer,
private readonly EmployeeService $employeeService,
) {}
/**
* @return array<int, string>
*/
public function readHeaders(string $path): array
{
$sheet = IOFactory::load($path)->getActiveSheet();
$headers = [];
foreach ($sheet->getRowIterator(1, 1) as $row) {
foreach ($row->getCellIterator() as $cell) {
$value = trim((string) $cell->getValue());
if ($value !== '') {
$headers[] = $value;
}
}
}
return $headers;
}
/**
* @return array<int, array<string, mixed>>
*/
public function readRows(string $path, ?int $limit = null): array
{
$sheet = IOFactory::load($path)->getActiveSheet();
$headers = $this->readHeaders($path);
$rows = [];
$highestRow = $sheet->getHighestDataRow();
for ($rowIndex = 2; $rowIndex <= $highestRow; $rowIndex++) {
$rowData = [];
$isEmpty = true;
foreach ($headers as $columnIndex => $header) {
$columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($columnIndex + 1);
$value = $sheet->getCell($columnLetter . $rowIndex)->getFormattedValue();
$rowData[$header] = is_string($value) ? trim($value) : $value;
if ($rowData[$header] !== null && $rowData[$header] !== '') {
$isEmpty = false;
}
}
if (! $isEmpty) {
$rows[] = $rowData;
}
if ($limit !== null && count($rows) >= $limit) {
break;
}
}
return $rows;
}
/**
* @param array<string, string|null> $columnMapping
* @return array<int, array<string, mixed>>
*/
public function mapRows(array $rows, array $columnMapping): array
{
return array_map(function (array $row) use ($columnMapping): array {
$mapped = [];
foreach ($columnMapping as $field => $header) {
if ($header === null || $header === '') {
continue;
}
$mapped[$field] = $row[$header] ?? null;
}
return $mapped;
}, $rows);
}
/**
* @param array<int, array<string, mixed>> $mappedRows
* @return array<int, array{row: int, errors: array<int, string>}>
*/
public function validatePreview(ImportType $type, array $mappedRows, int $limit = 10): array
{
$previewRows = array_slice($mappedRows, 0, $limit);
$results = [];
foreach ($previewRows as $index => $row) {
$results[] = [
'row' => $index + 2,
'errors' => $this->validateRow($type, $row),
];
}
return $results;
}
/**
* @param array<int, array<string, mixed>> $mappedRows
*/
public function import(ImportType $type, array $mappedRows, ?int $userId = null): ImportResultDto
{
$imported = 0;
$skipped = 0;
$failed = 0;
$errors = [];
foreach ($mappedRows as $index => $row) {
$rowNumber = $index + 2;
$validationErrors = $this->validateRow($type, $row);
if ($validationErrors !== []) {
$failed++;
$errors[] = "Row {$rowNumber}: " . implode(' ', $validationErrors);
continue;
}
if ($this->isDuplicate($type, $row)) {
$skipped++;
continue;
}
try {
$this->persistRow($type, $row, $userId);
$imported++;
} catch (\Throwable $exception) {
$failed++;
$errors[] = "Row {$rowNumber}: {$exception->getMessage()}";
}
}
return new ImportResultDto(
total: count($mappedRows),
imported: $imported,
skipped: $skipped,
failed: $failed,
errors: array_slice($errors, 0, 50),
);
}
/**
* @param array<string, mixed> $row
* @return array<int, string>
*/
private function validateRow(ImportType $type, array $row): array
{
return match ($type) {
ImportType::Employees => $this->employeeTransformer->validate(EmployeeRowDto::fromMappedRow($row)),
ImportType::Vacations => $this->vacationTransformer->validate(VacationRowDto::fromMappedRow($row)),
ImportType::Bonuses => $this->bonusTransformer->validate(BonusRowDto::fromMappedRow($row)),
ImportType::Reports => $this->reportTransformer->validate(ReportRowDto::fromMappedRow($row)),
};
}
/**
* @param array<string, mixed> $row
*/
private function isDuplicate(ImportType $type, array $row): bool
{
return match ($type) {
ImportType::Employees => $this->employeeTransformer->isDuplicate(EmployeeRowDto::fromMappedRow($row)),
ImportType::Vacations => $this->vacationTransformer->isDuplicate(VacationRowDto::fromMappedRow($row)),
ImportType::Bonuses => $this->bonusTransformer->isDuplicate(BonusRowDto::fromMappedRow($row)),
ImportType::Reports => $this->reportTransformer->isDuplicate(ReportRowDto::fromMappedRow($row)),
};
}
/**
* @param array<string, mixed> $row
*/
private function persistRow(ImportType $type, array $row, ?int $userId): void
{
match ($type) {
ImportType::Employees => $this->employeeService->create(
$this->employeeTransformer->toAttributes(EmployeeRowDto::fromMappedRow($row)),
),
ImportType::Vacations => Vacation::create(
$this->vacationTransformer->toAttributes(VacationRowDto::fromMappedRow($row)),
),
ImportType::Bonuses => Bonus::create(
$this->bonusTransformer->toAttributes(BonusRowDto::fromMappedRow($row)),
),
ImportType::Reports => DisciplinaryReport::create(
$this->reportTransformer->toAttributes(ReportRowDto::fromMappedRow($row), $userId),
),
};
}
}