Added more imports

This commit is contained in:
Constantin Graf
2024-03-11 19:12:51 +01:00
parent 77e7a63b83
commit 66a1d8a38b
52 changed files with 1646 additions and 56 deletions

View File

@@ -4,9 +4,11 @@ declare(strict_types=1);
namespace App\Service\Import;
use App\Service\Import\Importers\ImportException;
use Closure;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;
/**
* @template TModel of Model
@@ -23,9 +25,15 @@ class ImportDatabaseHelper
*/
private array $identifiers;
/**
* @var array<string, string>|null
*/
private ?array $mapIdentifierToKey = null;
private array $mapNewAttach = [];
/**
* @var array<string, string>
*/
private array $mapExternalIdentifierToInternalIdentifier = [];
private bool $attachToExisting;
@@ -57,7 +65,11 @@ class ImportDatabaseHelper
return (new $this->model)->query();
}
private function createEntity(array $identifierData, array $createValues): string
/**
* @param array<string, mixed> $identifierData
* @param array<string, mixed> $createValues
*/
private function createEntity(array $identifierData, array $createValues, ?string $externalIdentifier): string
{
$model = new $this->model();
foreach ($identifierData as $identifier => $identifierValue) {
@@ -72,34 +84,97 @@ class ImportDatabaseHelper
($this->afterCreate)($model);
}
$this->mapIdentifierToKey[$this->getHash($identifierData)] = $model->getKey();
$hash = $this->getHash($identifierData);
$this->mapIdentifierToKey[$hash] = $model->getKey();
$this->createdCount++;
if ($externalIdentifier !== null) {
$this->mapExternalIdentifierToInternalIdentifier[$externalIdentifier] = $hash;
}
return $model->getKey();
}
/**
* @param array<string, mixed> $data
*/
private function getHash(array $data): string
{
return md5(json_encode($data));
$jsonData = json_encode($data);
if ($jsonData === false) {
throw new \RuntimeException('Failed to encode data to JSON');
}
return md5($jsonData);
}
public function getKey(array $identifierData, array $createValues = []): string
/**
* @param array<string, mixed> $identifierData
* @param array<string, mixed> $createValues
*
* @throws ImportException
*/
public function getKey(array $identifierData, array $createValues = [], ?string $externalIdentifier = null): string
{
$this->checkMap();
$this->validateIdentifierData($identifierData);
$hash = $this->getHash($identifierData);
if ($this->attachToExisting) {
$key = $this->mapIdentifierToKey[$hash] ?? null;
if ($key !== null) {
if ($externalIdentifier !== null) {
$this->mapExternalIdentifierToInternalIdentifier[$externalIdentifier] = $hash;
}
Log::debug('HIT', [
'class' => $this->model,
]);
return $key;
}
return $this->createEntity($identifierData, $createValues);
Log::debug('MISS', [
'class' => $this->model,
]);
return $this->createEntity($identifierData, $createValues, $externalIdentifier);
} else {
throw new \RuntimeException('Not implemented');
}
}
/**
* @param array<string, mixed> $identifierData
*
* @throws ImportException
*/
private function validateIdentifierData(array $identifierData): void
{
if (array_keys($identifierData) !== $this->identifiers) {
throw new ImportException('Invalid identifier data');
}
}
public function getKeyByExternalIdentifier(string $externalIdentifier): ?string
{
$hash = $this->mapExternalIdentifierToInternalIdentifier[$externalIdentifier] ?? null;
if ($hash === null) {
return null;
}
return $this->mapIdentifierToKey[$hash] ?? null;
}
/**
* @return array<string>
*/
public function getExternalIds(): array
{
// Note: Otherwise the external ids are integers
return array_map(fn ($value) => (string) $value, array_keys($this->mapExternalIdentifierToInternalIdentifier));
}
private function checkMap(): void
{
if ($this->mapIdentifierToKey === null) {

View File

@@ -16,13 +16,13 @@ class ImportService
/**
* @throws ImportException
*/
public function import(Organization $organization, string $importerType, string $data, array $options): ReportDto
public function import(Organization $organization, string $importerType, string $data): ReportDto
{
/** @var ImporterContract $importer */
$importer = app(ImporterProvider::class)->getImporter($importerType);
$importer->init($organization);
DB::transaction(function () use (&$importer, &$data, &$options, &$organization) {
$importer->importData($data, $options);
DB::transaction(function () use (&$importer, &$data) {
$importer->importData($data);
});
return $importer->getReport();

View File

@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
namespace App\Service\Import\Importers;
use App\Models\Client;
use App\Models\Organization;
use App\Models\Project;
use App\Models\Task;
use App\Service\ColorService;
use App\Service\Import\ImportDatabaseHelper;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use League\Csv\Exception as CsvException;
use League\Csv\Reader;
class ClockifyProjectsImporter implements ImporterContract
{
private Organization $organization;
/**
* @var ImportDatabaseHelper<Project>
*/
private ImportDatabaseHelper $projectImportHelper;
/**
* @var ImportDatabaseHelper<Client>
*/
private ImportDatabaseHelper $clientImportHelper;
/**
* @var ImportDatabaseHelper<Task>
*/
private ImportDatabaseHelper $taskImportHelper;
#[\Override]
public function init(Organization $organization): void
{
$this->organization = $organization;
$this->projectImportHelper = new ImportDatabaseHelper(Project::class, ['name', 'organization_id'], true, function (Builder $builder) {
return $builder->where('organization_id', $this->organization->id);
});
$this->clientImportHelper = new ImportDatabaseHelper(Client::class, ['name', 'organization_id'], true, function (Builder $builder) {
return $builder->where('organization_id', $this->organization->id);
});
$this->taskImportHelper = new ImportDatabaseHelper(Task::class, ['name', 'project_id', 'organization_id'], true, function (Builder $builder) {
return $builder->where('organization_id', $this->organization->id);
});
}
/**
* @throws ImportException
*/
#[\Override]
public function importData(string $data): void
{
try {
$colorService = app(ColorService::class);
$reader = Reader::createFromString($data);
$reader->setHeaderOffset(0);
$reader->setDelimiter(',');
$header = $reader->getHeader();
$this->validateHeader($header);
$records = $reader->getRecords();
foreach ($records as $record) {
$clientId = null;
if ($record['Client'] !== '') {
$clientId = $this->clientImportHelper->getKey([
'name' => $record['Client'],
'organization_id' => $this->organization->id,
]);
}
$projectId = null;
if ($record['Name'] !== '') {
$projectId = $this->projectImportHelper->getKey([
'name' => $record['Name'],
'organization_id' => $this->organization->id,
], [
'client_id' => $clientId,
'color' => $colorService->getRandomColor(),
]);
}
if ($record['Tasks'] !== '') {
$tasks = explode(', ', $record['Tasks']);
foreach ($tasks as $task) {
if (strlen($task) > 255) {
throw new ImportException('Task is too long');
}
$taskId = $this->taskImportHelper->getKey([
'name' => $task,
'project_id' => $projectId,
'organization_id' => $this->organization->id,
]);
}
}
}
} catch (ImportException $exception) {
throw $exception;
} catch (CsvException $exception) {
throw new ImportException('Invalid CSV data');
} catch (Exception $exception) {
report($exception);
throw new ImportException('Unknown error');
}
}
/**
* @param array<string> $header
*
* @throws ImportException
*/
private function validateHeader(array $header): void
{
$requiredFields = [
'Name',
'Client',
'Status',
'Visibility',
'Billability',
'Tasks',
];
foreach ($requiredFields as $requiredField) {
if (! in_array($requiredField, $header, true)) {
throw new ImportException('Invalid CSV header, missing field: '.$requiredField);
}
}
}
#[\Override]
public function getReport(): ReportDto
{
return new ReportDto(
clientsCreated: $this->clientImportHelper->getCreatedCount(),
projectsCreated: $this->projectImportHelper->getCreatedCount(),
tasksCreated: $this->taskImportHelper->getCreatedCount(),
timeEntriesCreated: 0,
tagsCreated: 0,
usersCreated: 0,
);
}
}

View File

@@ -0,0 +1,227 @@
<?php
declare(strict_types=1);
namespace App\Service\Import\Importers;
use App\Models\Client;
use App\Models\Organization;
use App\Models\Project;
use App\Models\Tag;
use App\Models\Task;
use App\Models\TimeEntry;
use App\Models\User;
use App\Service\ColorService;
use App\Service\Import\ImportDatabaseHelper;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use League\Csv\Exception as CsvException;
use League\Csv\Reader;
class ClockifyTimeEntriesImporter implements ImporterContract
{
private Organization $organization;
/**
* @var ImportDatabaseHelper<User>
*/
private ImportDatabaseHelper $userImportHelper;
/**
* @var ImportDatabaseHelper<Project>
*/
private ImportDatabaseHelper $projectImportHelper;
/**
* @var ImportDatabaseHelper<Tag>
*/
private ImportDatabaseHelper $tagImportHelper;
/**
* @var ImportDatabaseHelper<Client>
*/
private ImportDatabaseHelper $clientImportHelper;
/**
* @var ImportDatabaseHelper<Task>
*/
private ImportDatabaseHelper $taskImportHelper;
private int $timeEntriesCreated;
#[\Override]
public function init(Organization $organization): void
{
$this->organization = $organization;
$this->userImportHelper = new ImportDatabaseHelper(User::class, ['email'], true, function (Builder $builder) {
/** @var Builder<User> $builder */
return $builder->belongsToOrganization($this->organization);
}, function (User $user) {
$user->organizations()->attach($this->organization, [
'role' => 'placeholder',
]);
});
$this->projectImportHelper = new ImportDatabaseHelper(Project::class, ['name', 'organization_id'], true, function (Builder $builder) {
return $builder->where('organization_id', $this->organization->id);
});
$this->tagImportHelper = new ImportDatabaseHelper(Tag::class, ['name', 'organization_id'], true, function (Builder $builder) {
return $builder->where('organization_id', $this->organization->id);
});
$this->clientImportHelper = new ImportDatabaseHelper(Client::class, ['name', 'organization_id'], true, function (Builder $builder) {
return $builder->where('organization_id', $this->organization->id);
});
$this->taskImportHelper = new ImportDatabaseHelper(Task::class, ['name', 'project_id', 'organization_id'], true, function (Builder $builder) {
return $builder->where('organization_id', $this->organization->id);
});
$this->timeEntriesCreated = 0;
}
/**
* @return array<string>
*
* @throws ImportException
*/
private function getTags(string $tags): array
{
if (trim($tags) === '') {
return [];
}
$tagsParsed = explode(', ', $tags);
$tagIds = [];
foreach ($tagsParsed as $tagParsed) {
if (strlen($tagParsed) > 255) {
throw new ImportException('Tag is too long');
}
$tagId = $this->tagImportHelper->getKey([
'name' => $tagParsed,
'organization_id' => $this->organization->id,
]);
$tagIds[] = $tagId;
}
return $tagIds;
}
/**
* @throws ImportException
*/
#[\Override]
public function importData(string $data): void
{
try {
$colorService = app(ColorService::class);
$reader = Reader::createFromString($data);
$reader->setHeaderOffset(0);
$reader->setDelimiter(',');
$header = $reader->getHeader();
$this->validateHeader($header);
$records = $reader->getRecords();
foreach ($records as $record) {
$userId = $this->userImportHelper->getKey([
'email' => $record['Email'],
], [
'name' => $record['User'],
'is_placeholder' => true,
]);
$clientId = null;
if ($record['Client'] !== '') {
$clientId = $this->clientImportHelper->getKey([
'name' => $record['Client'],
'organization_id' => $this->organization->id,
]);
}
$projectId = null;
if ($record['Project'] !== '') {
$projectId = $this->projectImportHelper->getKey([
'name' => $record['Project'],
'organization_id' => $this->organization->id,
], [
'client_id' => $clientId,
'color' => $colorService->getRandomColor(),
]);
}
$taskId = null;
if ($record['Task'] !== '') {
$taskId = $this->taskImportHelper->getKey([
'name' => $record['Task'],
'project_id' => $projectId,
'organization_id' => $this->organization->id,
]);
}
$timeEntry = new TimeEntry();
$timeEntry->user_id = $userId;
$timeEntry->task_id = $taskId;
$timeEntry->project_id = $projectId;
$timeEntry->organization_id = $this->organization->id;
$timeEntry->description = $record['Description'];
if (! in_array($record['Billable'], ['Yes', 'No'], true)) {
throw new ImportException('Invalid billable value');
}
$timeEntry->billable = $record['Billable'] === 'Yes';
$timeEntry->tags = $this->getTags($record['Tags']);
$start = Carbon::createFromFormat('m/d/Y H:i:s A', $record['Start Date'].' '.$record['Start Time'], 'UTC');
if ($start === false) {
throw new ImportException('Start date ("'.$record['Start Date'].'") or time ("'.$record['Start Time'].'") are invalid');
}
$timeEntry->start = $start;
$end = Carbon::createFromFormat('m/d/Y H:i:s A', $record['End Date'].' '.$record['End Time'], 'UTC');
if ($end === false) {
throw new ImportException('End date ("'.$record['End Date'].'") or time ("'.$record['End Time'].'") are invalid');
}
$timeEntry->end = $end;
$timeEntry->save();
$this->timeEntriesCreated++;
}
} catch (ImportException $exception) {
throw $exception;
} catch (CsvException $exception) {
throw new ImportException('Invalid CSV data');
} catch (Exception $exception) {
report($exception);
throw new ImportException('Unknown error');
}
}
/**
* @param array<string> $header
*
* @throws ImportException
*/
private function validateHeader(array $header): void
{
$requiredFields = [
'Project',
'Client',
'Description',
'Task',
'User',
'Group',
'Email',
'Tags',
'Billable',
'Start Date',
'Start Time',
'End Date',
'End Time',
];
foreach ($requiredFields as $requiredField) {
if (! in_array($requiredField, $header, true)) {
throw new ImportException('Invalid CSV header, missing field: '.$requiredField);
}
}
}
#[\Override]
public function getReport(): ReportDto
{
return new ReportDto(
clientsCreated: $this->clientImportHelper->getCreatedCount(),
projectsCreated: $this->projectImportHelper->getCreatedCount(),
tasksCreated: $this->taskImportHelper->getCreatedCount(),
timeEntriesCreated: $this->timeEntriesCreated,
tagsCreated: $this->tagImportHelper->getCreatedCount(),
usersCreated: $this->userImportHelper->getCreatedCount(),
);
}
}

View File

@@ -10,7 +10,7 @@ interface ImporterContract
{
public function init(Organization $organization): void;
public function importData(string $data, array $options): void;
public function importData(string $data): void;
public function getReport(): ReportDto;
}

View File

@@ -11,6 +11,9 @@ class ImporterProvider
*/
private array $importers = [
'toggl_time_entries' => TogglTimeEntriesImporter::class,
'toggl_data_importer' => TogglDataImporter::class,
'clockify_time_entries' => ClockifyTimeEntriesImporter::class,
'clockify_projects' => ClockifyProjectsImporter::class,
];
/**

View File

@@ -0,0 +1,194 @@
<?php
declare(strict_types=1);
namespace App\Service\Import\Importers;
use App\Models\Client;
use App\Models\Organization;
use App\Models\Project;
use App\Models\Tag;
use App\Models\Task;
use App\Models\User;
use App\Service\ColorService;
use App\Service\Import\ImportDatabaseHelper;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Spatie\TemporaryDirectory\TemporaryDirectory;
use ZipArchive;
class TogglDataImporter implements ImporterContract
{
private Organization $organization;
/**
* @var ImportDatabaseHelper<User>
*/
private ImportDatabaseHelper $userImportHelper;
/**
* @var ImportDatabaseHelper<Project>
*/
private ImportDatabaseHelper $projectImportHelper;
/**
* @var ImportDatabaseHelper<Tag>
*/
private ImportDatabaseHelper $tagImportHelper;
/**
* @var ImportDatabaseHelper<Client>
*/
private ImportDatabaseHelper $clientImportHelper;
/**
* @var ImportDatabaseHelper<Task>
*/
private ImportDatabaseHelper $taskImportHelper;
private ColorService $colorService;
#[\Override]
public function init(Organization $organization): void
{
$this->organization = $organization;
$this->userImportHelper = new ImportDatabaseHelper(User::class, ['email'], true, function (Builder $builder) {
/** @var Builder<User> $builder */
return $builder->belongsToOrganization($this->organization);
}, function (User $user) {
$user->organizations()->attach($this->organization, [
'role' => 'placeholder',
]);
});
$this->projectImportHelper = new ImportDatabaseHelper(Project::class, ['name', 'organization_id'], true, function (Builder $builder) {
return $builder->where('organization_id', $this->organization->id);
});
$this->tagImportHelper = new ImportDatabaseHelper(Tag::class, ['name', 'organization_id'], true, function (Builder $builder) {
return $builder->where('organization_id', $this->organization->id);
});
$this->clientImportHelper = new ImportDatabaseHelper(Client::class, ['name', 'organization_id'], true, function (Builder $builder) {
return $builder->where('organization_id', $this->organization->id);
});
$this->taskImportHelper = new ImportDatabaseHelper(Task::class, ['name', 'project_id', 'organization_id'], true, function (Builder $builder) {
return $builder->where('organization_id', $this->organization->id);
});
$this->colorService = app(ColorService::class);
}
/**
* @throws ImportException
*/
#[\Override]
public function importData(string $data): void
{
try {
$zip = new ZipArchive();
$temporaryDirectory = TemporaryDirectory::make();
file_put_contents($temporaryDirectory->path('import.zip'), $data);
$zip->open($temporaryDirectory->path('import.zip'), ZipArchive::RDONLY);
$temporaryDirectory = TemporaryDirectory::make();
$zip->extractTo($temporaryDirectory->path());
$zip->close();
$clientsFileContent = file_get_contents($temporaryDirectory->path('clients.json'));
if ($clientsFileContent === false) {
throw new ImportException('File clients.json missing in ZIP');
}
$clients = json_decode($clientsFileContent);
$projectsFileContent = file_get_contents($temporaryDirectory->path('projects.json'));
if ($projectsFileContent === false) {
throw new ImportException('File projects.json missing in ZIP');
}
$projects = json_decode($projectsFileContent);
$tagsFileContent = file_get_contents($temporaryDirectory->path('tags.json'));
if ($tagsFileContent === false) {
throw new ImportException('File tags.json missing in ZIP');
}
$tags = json_decode($tagsFileContent);
$workspaceUsersFileContent = file_get_contents($temporaryDirectory->path('workspace_users.json'));
if ($workspaceUsersFileContent === false) {
throw new ImportException('File workspace_users.json missing in ZIP');
}
$workspaceUsers = json_decode($workspaceUsersFileContent);
foreach ($clients as $client) {
$this->clientImportHelper->getKey([
'name' => $client->name,
'organization_id' => $this->organization->id,
], [], (string) $client->id);
}
foreach ($tags as $tag) {
$this->tagImportHelper->getKey([
'name' => $tag->name,
'organization_id' => $this->organization->id,
], [], (string) $tag->id);
}
foreach ($projects as $project) {
$clientId = null;
if ($project->client_id !== null) {
$clientId = $this->clientImportHelper->getKeyByExternalIdentifier((string) $project->client_id);
if ($clientId === null) {
throw new Exception('Client does not exist');
}
}
if (! $this->colorService->isValid($project->color)) {
throw new ImportException('Invalid color');
}
$this->projectImportHelper->getKey([
'name' => $project->name,
'organization_id' => $this->organization->getKey(),
], [
'client_id' => $clientId,
'color' => $project->color,
], (string) $project->id);
}
foreach ($workspaceUsers as $workspaceUser) {
$this->userImportHelper->getKey([
'email' => $workspaceUser->email,
], [
'name' => $workspaceUser->name,
'is_placeholder' => true,
], (string) $workspaceUser->id);
}
$projectIds = $this->projectImportHelper->getExternalIds();
foreach ($projectIds as $projectIdExternal) {
$tasksFileContent = file_get_contents($temporaryDirectory->path('tasks/'.$projectIdExternal.'.json'));
if ($tasksFileContent === false) {
throw new ImportException('File tasks/'.$projectIdExternal.'.json missing in ZIP');
}
$tasks = json_decode($tasksFileContent);
foreach ($tasks as $task) {
$projectId = $this->projectImportHelper->getKeyByExternalIdentifier((string) $projectIdExternal);
if ($projectId === null) {
throw new Exception('Project does not exist');
}
$this->taskImportHelper->getKey([
'name' => $task->name,
'project_id' => $projectId,
'organization_id' => $this->organization->getKey(),
], [], (string) $task->id);
}
}
} catch (ImportException $exception) {
throw $exception;
} catch (Exception $exception) {
report($exception);
throw new ImportException('Unknown error');
}
}
#[\Override]
public function getReport(): ReportDto
{
return new ReportDto(
clientsCreated: $this->clientImportHelper->getCreatedCount(),
projectsCreated: $this->projectImportHelper->getCreatedCount(),
tasksCreated: $this->taskImportHelper->getCreatedCount(),
timeEntriesCreated: 0,
tagsCreated: $this->tagImportHelper->getCreatedCount(),
usersCreated: $this->userImportHelper->getCreatedCount(),
);
}
}

View File

@@ -13,6 +13,7 @@ use App\Models\TimeEntry;
use App\Models\User;
use App\Service\ColorService;
use App\Service\Import\ImportDatabaseHelper;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use League\Csv\Exception as CsvException;
@@ -22,14 +23,29 @@ class TogglTimeEntriesImporter implements ImporterContract
{
private Organization $organization;
/**
* @var ImportDatabaseHelper<User>
*/
private ImportDatabaseHelper $userImportHelper;
/**
* @var ImportDatabaseHelper<Project>
*/
private ImportDatabaseHelper $projectImportHelper;
/**
* @var ImportDatabaseHelper<Tag>
*/
private ImportDatabaseHelper $tagImportHelper;
/**
* @var ImportDatabaseHelper<Client>
*/
private ImportDatabaseHelper $clientImportHelper;
/**
* @var ImportDatabaseHelper<Task>
*/
private ImportDatabaseHelper $taskImportHelper;
private int $timeEntriesCreated;
@@ -39,14 +55,13 @@ class TogglTimeEntriesImporter implements ImporterContract
{
$this->organization = $organization;
$this->userImportHelper = new ImportDatabaseHelper(User::class, ['email'], true, function (Builder $builder) {
return $builder->whereHas('organizations', function (Builder $builder): Builder {
/** @var Builder<Organization> $builder */
return $builder->whereKey($this->organization->getKey());
});
/** @var Builder<User> $builder */
return $builder->belongsToOrganization($this->organization);
}, function (User $user) {
$user->organizations()->attach([$this->organization->id]);
$user->organizations()->attach($this->organization, [
'role' => 'placeholder',
]);
});
// TODO: user special after import
$this->projectImportHelper = new ImportDatabaseHelper(Project::class, ['name', 'organization_id'], true, function (Builder $builder) {
return $builder->where('organization_id', $this->organization->id);
});
@@ -62,6 +77,11 @@ class TogglTimeEntriesImporter implements ImporterContract
$this->timeEntriesCreated = 0;
}
/**
* @return array<string>
*
* @throws ImportException
*/
private function getTags(string $tags): array
{
if (trim($tags) === '') {
@@ -87,7 +107,7 @@ class TogglTimeEntriesImporter implements ImporterContract
* @throws ImportException
*/
#[\Override]
public function importData(string $data, array $options): void
public function importData(string $data): void
{
try {
$colorService = app(ColorService::class);
@@ -140,17 +160,34 @@ class TogglTimeEntriesImporter implements ImporterContract
}
$timeEntry->billable = $record['Billable'] === 'Yes';
$timeEntry->tags = $this->getTags($record['Tags']);
$timeEntry->start = Carbon::createFromFormat('Y-m-d H:i:s', $record['Start date'].' '.$record['Start time'], 'UTC');
$timeEntry->end = Carbon::createFromFormat('Y-m-d H:i:s', $record['End date'].' '.$record['End time'], 'UTC');
$start = Carbon::createFromFormat('Y-m-d H:i:s', $record['Start date'].' '.$record['Start time'], 'UTC');
if ($start === false) {
throw new ImportException('Start date ("'.$record['Start date'].'") or time ("'.$record['Start time'].'") are invalid');
}
$timeEntry->start = $start;
$end = Carbon::createFromFormat('Y-m-d H:i:s', $record['End date'].' '.$record['End time'], 'UTC');
if ($end === false) {
throw new ImportException('End date ("'.$record['End date'].'") or time ("'.$record['End time'].'") are invalid');
}
$timeEntry->end = $end;
$timeEntry->save();
$this->timeEntriesCreated++;
}
} catch (ImportException $exception) {
throw $exception;
} catch (CsvException $exception) {
throw new ImportException('Invalid CSV data');
} catch (Exception $exception) {
report($exception);
throw new ImportException('Unknown error');
}
}
/**
* @param array<string> $header
*
* @throws ImportException
*/
private function validateHeader(array $header): void
{
$requiredFields = [