Skip to content

Commit 519582d

Browse files
committed
[Added] CSV Import Module
1 parent d325cb4 commit 519582d

File tree

6 files changed

+297
-0
lines changed

6 files changed

+297
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
use Coderstm\Traits\Helpers;
4+
use Illuminate\Database\Migrations\Migration;
5+
use Illuminate\Database\Schema\Blueprint;
6+
use Illuminate\Support\Facades\Schema;
7+
8+
return new class extends Migration
9+
{
10+
use Helpers;
11+
12+
/**
13+
* Run the migrations.
14+
*/
15+
public function up(): void
16+
{
17+
Schema::dropIfExists('imports');
18+
Schema::create('imports', function (Blueprint $table) {
19+
$table->id();
20+
$table->string('model')->nullable();
21+
$table->unsignedBigInteger('file_id')->nullable();
22+
$table->unsignedBigInteger('user_id')->nullable();
23+
$table->string('status')->nullable();
24+
$table->{$this->jsonable()}('options')->nullable();
25+
$table->{$this->jsonable()}('success')->nullable();
26+
$table->{$this->jsonable()}('failed')->nullable();
27+
$table->{$this->jsonable()}('skipped')->nullable();
28+
$table->timestamps();
29+
});
30+
}
31+
32+
/**
33+
* Reverse the migrations.
34+
*/
35+
public function down(): void
36+
{
37+
Schema::dropIfExists('imports');
38+
}
39+
};
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Coderstm\Exceptions;
4+
5+
use Exception;
6+
7+
class ImportFailedException extends Exception
8+
{
9+
public function __construct($message = null)
10+
{
11+
parent::__construct($message ?? 'Record with the same email already exists.');
12+
}
13+
}
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Coderstm\Exceptions;
4+
5+
use Exception;
6+
7+
class ImportSkippedException extends Exception
8+
{
9+
public function __construct($message = null)
10+
{
11+
parent::__construct($message ?? 'Already created or updated from CSV.');
12+
}
13+
}

src/Jobs/ProcessCsvImport.php

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
namespace Coderstm\Jobs;
4+
5+
use Coderstm\Exceptions\ImportSkippedException;
6+
use Coderstm\Models\Import;
7+
use Coderstm\Notifications\ImportCompletedNotification;
8+
use League\Csv\Reader;
9+
use Illuminate\Bus\Queueable;
10+
use Illuminate\Support\Facades\DB;
11+
use Illuminate\Queue\SerializesModels;
12+
use Illuminate\Queue\InteractsWithQueue;
13+
use Illuminate\Contracts\Queue\ShouldQueue;
14+
use Illuminate\Foundation\Bus\Dispatchable;
15+
16+
class ProcessCsvImport implements ShouldQueue
17+
{
18+
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
19+
20+
public Import $import;
21+
public string $model;
22+
public string $filePath;
23+
public array $options;
24+
25+
/**
26+
* Create a new job instance.
27+
*/
28+
public function __construct(Import $import)
29+
{
30+
$this->import = $import;
31+
$this->model = $import->model;
32+
$this->filePath = $import->file->path();
33+
$this->options = $import->options;
34+
}
35+
36+
/**
37+
* Execute the job.
38+
*/
39+
public function handle(): void
40+
{
41+
$csv = Reader::createFromPath($this->filePath, 'r');
42+
$csv->setHeaderOffset(0);
43+
$csv->setDelimiter(',');
44+
45+
// Normalize CSV headers to remove newlines
46+
$csvHeaders = array_map('trim', $csv->getHeader());
47+
$mappedHeaders = $this->model::getMappedAttributes();
48+
49+
// Map $headers from $mapped
50+
$finalHeaders = [];
51+
foreach ($csvHeaders as $header) {
52+
if (isset($mappedHeaders[$header])) {
53+
$finalHeaders[] = $mappedHeaders[$header];
54+
} else {
55+
$finalHeaders[] = $header;
56+
}
57+
}
58+
59+
$this->import->update(['status' => Import::STATUS_PROCESSING]);
60+
61+
// Begin a database transaction
62+
DB::beginTransaction();
63+
64+
try {
65+
foreach ($csv->getRecords($finalHeaders) as $key => $row) {
66+
try {
67+
$this->model::createFromCsv($row, $this->options);
68+
$this->import->addLogs("success", $key);
69+
} catch (\Exception $e) {
70+
if ($e instanceof ImportSkippedException) {
71+
$this->import->addLogs("skipped", $key);
72+
} else {
73+
$this->import->addLogs("failed", $key);
74+
}
75+
//throw $e;
76+
}
77+
}
78+
79+
// Commit the transaction if all records are successfully processed
80+
DB::commit();
81+
82+
// Update import status to completed
83+
$this->import->update(['status' => Import::STATUS_COMPLETED]);
84+
admin_notify(new ImportCompletedNotification($this->import));
85+
} catch (\Exception $e) {
86+
// Rollback the transaction in case of an error
87+
DB::rollback();
88+
throw $e;
89+
}
90+
}
91+
}

src/Models/Import.php

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
namespace Coderstm\Models;
4+
5+
use Coderstm\Models\File;
6+
use Coderstm\Traits\SerializeDate;
7+
use Illuminate\Database\Eloquent\Model;
8+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
9+
10+
class Import extends Model
11+
{
12+
use SerializeDate;
13+
14+
const STATUS_PENDING = 'Pending';
15+
const STATUS_PROCESSING = 'Processing';
16+
const STATUS_COMPLETED = 'Completed';
17+
const STATUS_FAILED = 'Failed';
18+
19+
protected $fillable = [
20+
'model',
21+
'file_id',
22+
'user_id',
23+
'status',
24+
'options',
25+
'success',
26+
'failed',
27+
'skipped',
28+
];
29+
30+
protected $casts = [
31+
'options' => 'array',
32+
'success' => 'array',
33+
'failed' => 'array',
34+
'skipped' => 'array',
35+
];
36+
37+
public function user(): BelongsTo
38+
{
39+
return $this->belongsTo(Admin::class, 'user_id');
40+
}
41+
42+
public function file(): BelongsTo
43+
{
44+
return $this->belongsTo(File::class, 'file_id');
45+
}
46+
47+
public function addLogs($type, $line): void
48+
{
49+
$lines = $this->{$type} ?? [];
50+
$this->update([
51+
$type => array_merge($lines, [$line])
52+
]);
53+
}
54+
55+
public function getShortCodes(): array
56+
{
57+
return array_merge($this->user->getShortCodes(), [
58+
'{{IMPORT_MODEL}}' => class_basename($this->model),
59+
'{{IMPORT_STATUS}}' => $this->status,
60+
'{{IMPORT_SUCCESSED}}' => count($this->success ?? []),
61+
'{{IMPORT_FAILED}}' => count($this->failed ?? []),
62+
'{{IMPORT_SKIPPED}}' => count($this->skipped ?? []),
63+
]);
64+
}
65+
66+
protected static function booted()
67+
{
68+
parent::booted();
69+
static::creating(function ($model) {
70+
$model->status = static::STATUS_PENDING;
71+
$model->user_id = optional(user())->id;
72+
});
73+
}
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
namespace Coderstm\Notifications;
4+
5+
use Coderstm\Models\Import;
6+
use Coderstm\Models\Notification as Template;
7+
use Illuminate\Bus\Queueable;
8+
use Illuminate\Contracts\Queue\ShouldQueue;
9+
use Illuminate\Notifications\Messages\MailMessage;
10+
use Illuminate\Notifications\Notification;
11+
12+
class ImportCompletedNotification extends Notification
13+
{
14+
use Queueable;
15+
16+
public Import $import;
17+
public $subject;
18+
public $message;
19+
20+
/**
21+
* Create a new notification instance.
22+
*/
23+
public function __construct(Import $import)
24+
{
25+
$this->import = $import;
26+
27+
$template = Template::default('admin:import-completed');
28+
$shortCodes = $this->import->getShortCodes();
29+
30+
$this->subject = replace_short_code($template->subject, $shortCodes);
31+
$this->message = replace_short_code($template->content, $shortCodes);
32+
}
33+
34+
/**
35+
* Get the notification's delivery channels.
36+
*
37+
* @return array<int, string>
38+
*/
39+
public function via(object $notifiable): array
40+
{
41+
return ['mail'];
42+
}
43+
44+
/**
45+
* Get the mail representation of the notification.
46+
*/
47+
public function toMail(object $notifiable): MailMessage
48+
{
49+
return (new MailMessage)
50+
->subject($this->subject)
51+
->markdown('coderstm::emails.notification', [
52+
'message' => $this->message
53+
]);
54+
}
55+
56+
/**
57+
* Get the array representation of the notification.
58+
*
59+
* @return array<string, mixed>
60+
*/
61+
public function toArray(object $notifiable): array
62+
{
63+
return [
64+
//
65+
];
66+
}
67+
}

0 commit comments

Comments
 (0)