app/Models/Account/ImportJob.php (137 lines of code) (raw):
<?php
namespace App\Models\Account;
use App\Models\User\User;
use Sabre\VObject\Reader;
use Illuminate\Support\Arr;
use Sabre\VObject\Component\VCard;
use App\Services\VCard\ImportVCard;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\ValidationException;
use Sabre\VObject\Splitter\VCard as VCardReader;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
/**
* @property int $id
* @property Account $account
* @property int $account_id
* @property User $user
* @property int $user_id
* @property bool $failed
* @property string $failed_reason
* @property string $filename
* @property int $contacts_found
* @property int $contacts_skipped
* @property int $contacts_imported
* @property \Illuminate\Support\Carbon|null $started_at
* @property \Illuminate\Support\Carbon|null $ended_at
*/
class ImportJob extends Model
{
const VCARD_SKIPPED = true;
const VCARD_IMPORTED = false;
protected $table = 'import_jobs';
/**
* The physical vCard file on disk.
*/
public $physicalFile;
/**
* All individual entries in the vCard file.
*/
public $entries;
/**
* The attributes that aren't mass assignable.
*
* @var array
*/
protected $guarded = ['id'];
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['started_at', 'ended_at'];
/**
* Get the account record associated with the import job.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the user record associated with the import job.
*
* @return BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
/**
* Get the import jobs reports records associated with the account.
*
* @return HasMany
*/
public function importJobReports()
{
return $this->hasMany(ImportJobReport::class);
}
/**
* Process an import job.
*
* @return void
*/
public function process($behaviour = ImportVCard::BEHAVIOUR_ADD)
{
$this->initJob();
$this->getPhysicalFile();
$this->getEntries();
$this->processEntries($behaviour);
$this->deletePhysicalFile();
$this->endJob();
}
/**
* Perform preliminary steps to start the import job.
*
* @return void
*/
private function initJob(): void
{
$this->started_at = now();
$this->contacts_imported = 0;
$this->contacts_skipped = 0;
$this->save();
}
/**
* Perform the steps to finalize the import job.
*
* @return void
*/
private function endJob(): void
{
$this->ended_at = now();
$this->save();
}
/**
* Mark the import job as failed.
*
* @param string $reason
*
* @return void
*/
private function fail(string $reason): void
{
$this->failed = true;
$this->failed_reason = $reason;
$this->endJob();
}
/**
* Get the physical file (the vCard file).
*
* @return self
*/
private function getPhysicalFile()
{
try {
$this->physicalFile = Storage::disk('public')->get($this->filename);
} catch (FileNotFoundException $exception) {
$this->fail(trans('settings.import_vcard_file_not_found'));
}
return $this;
}
/**
* Delete the physical file from the disk.
*
* @return void
*/
private function deletePhysicalFile(): void
{
if (! Storage::disk('public')->delete($this->filename)) {
$this->fail(trans('settings.import_vcard_file_not_found'));
}
}
/**
* Get the number of matches in the vCard file.
*
* @return void
*/
private function getEntries()
{
$this->entries = new VCardReader($this->physicalFile, Reader::OPTION_FORGIVING + Reader::OPTION_IGNORE_INVALID_LINES);
}
/**
* Process all entries contained in the vCard file.
*
* @param string $behaviour
* @return void
*/
private function processEntries($behaviour = ImportVCard::BEHAVIOUR_ADD)
{
while (true) {
try {
$entry = $this->entries->getNext();
if (! $entry) {
// file end
break;
}
$this->contacts_found++;
} catch (\Throwable $e) {
$this->skipEntry('?', (string) $e);
continue;
}
$this->processSingleEntry($entry, $behaviour);
}
if ($this->contacts_found == 0) {
$this->fail(trans('settings.import_vcard_file_no_entries'));
}
}
/**
* Process a single vCard entry.
*
* @param string|VCard $entry
* @param string $behaviour
* @return void
*/
private function processSingleEntry($entry, $behaviour = ImportVCard::BEHAVIOUR_ADD): void
{
try {
$result = app(ImportVCard::class)->execute([
'account_id' => $this->account_id,
'user_id' => $this->user_id,
'entry' => $entry,
'behaviour' => $behaviour,
]);
} catch (ValidationException $e) {
$this->fail(implode(',', $e->validator->errors()->all()));
return;
}
if (Arr::has($result, 'error') && ! empty($result['error'])) {
$this->skipEntry($result['name'], $result['reason']);
return;
}
$this->contacts_imported++;
$this->fileImportJobReport($result['name'], self::VCARD_IMPORTED);
}
/**
* Skip the current entry.
*
* @param string $name
* @param string $reason
* @return void
*/
private function skipEntry($name, $reason = null): void
{
$this->fileImportJobReport($name, self::VCARD_SKIPPED, $reason);
$this->contacts_skipped++;
}
/**
* File an import job report for the current entry.
*
* @param string $name
* @param bool $status
* @param string $reason
* @return void
*/
private function fileImportJobReport($name, $status, $reason = null): void
{
$importJobReport = new ImportJobReport;
$importJobReport->account_id = $this->account_id;
$importJobReport->user_id = $this->user_id;
$importJobReport->import_job_id = $this->id;
$importJobReport->contact_information = trim($name);
$importJobReport->skipped = $status;
$importJobReport->skip_reason = $reason;
$importJobReport->save();
}
}