Skip to content

Commit 2c80725

Browse files
committed
Tambah proses record model
1 parent 8290acc commit 2c80725

14 files changed

+444
-15
lines changed

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@
1919
],
2020
"require": {
2121
"php": "^8.1",
22+
"maatwebsite/excel": "^3.1",
2223
"spatie/laravel-package-tools": "^1.15.0"
2324
},
2425
"require-dev": {
2526
"laravel/pint": "^1.10",
2627
"orchestra/testbench": "^8.0",
28+
"roave/security-advisories": "dev-latest",
2729
"vimeo/psalm": "^5.0"
2830
},
2931
"autoload": {

config/model-upload.php

Lines changed: 0 additions & 7 deletions
This file was deleted.

database/migrations/create_model_upload_table.php

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,38 @@
1010
{
1111
public function up(): void
1212
{
13-
Schema::create('laravel_model_upload_table', static function (Blueprint $table): void {
14-
$table->id();
15-
16-
// add fields
13+
Schema::create('model_upload_files', static function (Blueprint $table): void {
14+
$table->ulid('id')->primary();
15+
$table->foreignId('user_id');
16+
$table->string('model_type');
17+
$table->string('file_name');
18+
$table->string('storage_disk');
19+
$table->string('file_path');
20+
$table->string('state', 50);
21+
$table->longText('error_message')->nullable();
22+
$table->timestamps();
23+
});
1724

25+
Schema::create('model_upload_records', static function (Blueprint $table): void {
26+
$table->ulid('id')->primary();
27+
$table->foreignUlid('model_upload_file_id')
28+
->constrained()
29+
->cascadeOnUpdate()
30+
->cascadeOnDelete();
31+
$table->json('payload')->nullable();
32+
$table->json('meta')->nullable();
33+
$table->longText('error_message')->nullable();
34+
$table->string('model_id', 36)->nullable();
35+
$table->string('model_type')->nullable();
1836
$table->timestamps();
37+
38+
$table->index(['model_type', 'model_id']);
1939
});
2040
}
41+
42+
public function down(): void
43+
{
44+
Schema::dropIfExists('model_upload_records');
45+
Schema::dropIfExists('model_upload_files');
46+
}
2147
};
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace FromHome\ModelUpload\Actions;
6+
7+
use Illuminate\Http\UploadedFile;
8+
use FromHome\ModelUpload\ModelRecordImport;
9+
use Illuminate\Contracts\Auth\Authenticatable;
10+
use FromHome\ModelUpload\Enums\UploadFileState;
11+
use FromHome\ModelUpload\Models\ModelUploadFile;
12+
use Illuminate\Database\Eloquent\Relations\Relation;
13+
14+
final class StoreModelUploadFile
15+
{
16+
public function handle(Authenticatable $user, UploadedFile $uploadedFile, string $modelType, array $meta): ModelUploadFile
17+
{
18+
if (! \array_key_exists($modelType, Relation::morphMap())) {
19+
throw new \InvalidArgumentException(
20+
\sprintf('Invalid `modelType`, valid value is [%s]', \implode(',', Relation::morphMap()))
21+
);
22+
}
23+
24+
/** @var ModelUploadFile $file */
25+
$file = ModelUploadFile::query()->create([
26+
'user_id' => $user->getAuthIdentifier(),
27+
'model_type' => $modelType,
28+
'file_name' => $uploadedFile->getClientOriginalName(),
29+
'storage_disk' => \config('filesystems.default'),
30+
'file_path' => $uploadedFile->store(),
31+
'state' => UploadFileState::upload,
32+
]);
33+
34+
ModelRecordImport::new()
35+
->forFile($file)
36+
->withMeta($meta)
37+
->process();
38+
39+
return $file;
40+
}
41+
}

src/Enums/UploadFileState.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace FromHome\ModelUpload\Enums;
6+
7+
enum UploadFileState: string
8+
{
9+
case upload = 'upload';
10+
11+
case process = 'process';
12+
13+
case partialSuccess = 'partial success';
14+
15+
case error = 'error';
16+
17+
case done = 'done';
18+
}

src/Jobs/ProcessModelRecordJob.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace FromHome\ModelUpload\Jobs;
6+
7+
use Exception;
8+
use Illuminate\Bus\Queueable;
9+
use Illuminate\Queue\SerializesModels;
10+
use Illuminate\Queue\InteractsWithQueue;
11+
use Illuminate\Contracts\Queue\ShouldQueue;
12+
use Illuminate\Foundation\Bus\Dispatchable;
13+
use FromHome\ModelUpload\Enums\UploadFileState;
14+
use FromHome\ModelUpload\Models\ModelUploadFile;
15+
use FromHome\ModelUpload\Models\ModelUploadRecord;
16+
use FromHome\ModelUpload\Processor\RecordProcessorManager;
17+
18+
final class ProcessModelRecordJob implements ShouldQueue
19+
{
20+
use Dispatchable;
21+
use InteractsWithQueue;
22+
use Queueable;
23+
use SerializesModels;
24+
25+
public function __construct(
26+
private readonly ModelUploadFile $modelUploadFile
27+
) {
28+
}
29+
30+
public function handle(RecordProcessorManager $manager): void
31+
{
32+
$this->modelUploadFile->update([
33+
'state' => UploadFileState::process,
34+
]);
35+
36+
$action = $manager->getRecordProcessor(
37+
$this->modelUploadFile->getAttribute('model_type')
38+
);
39+
40+
if ($action === null) {
41+
$this->modelUploadFile->update([
42+
'state' => UploadFileState::error,
43+
'error_message' => \sprintf(
44+
'Invalid `null` action for %s type', $this->modelUploadFile->getAttribute('model_type')
45+
),
46+
]);
47+
48+
return;
49+
}
50+
51+
$errorCount = 0;
52+
$recordCount = $this->modelUploadFile->records()->count('id');
53+
54+
$this->modelUploadFile->records()->each(function (ModelUploadRecord $modelUpload) use ($action, &$errorCount): void {
55+
try {
56+
$model = $action->process($modelUpload);
57+
58+
$modelUpload->update([
59+
'model_id' => $model->getKey(),
60+
'model_type' => $this->modelUploadFile->getAttribute('model_type'),
61+
]);
62+
} catch (Exception $exception) {
63+
$modelUpload->update([
64+
'error_message' => $exception->getMessage(),
65+
'exception' => $exception->getTraceAsString(),
66+
]);
67+
68+
$errorCount++;
69+
}
70+
});
71+
72+
$errorState = $errorCount !== $recordCount ? UploadFileState::partialSuccess : UploadFileState::error;
73+
74+
$this->modelUploadFile->update([
75+
'state' => $errorCount === 0 ? UploadFileState::done : $errorState,
76+
]);
77+
}
78+
}

src/ModelRecordImport.php

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace FromHome\ModelUpload;
6+
7+
use Webmozart\Assert\Assert;
8+
use Maatwebsite\Excel\Concerns\ToModel;
9+
use Maatwebsite\Excel\Events\AfterImport;
10+
use Maatwebsite\Excel\Concerns\Importable;
11+
use Maatwebsite\Excel\Concerns\WithEvents;
12+
use Illuminate\Contracts\Queue\ShouldQueue;
13+
use Illuminate\Foundation\Bus\PendingDispatch;
14+
use Maatwebsite\Excel\Concerns\WithHeadingRow;
15+
use FromHome\ModelUpload\Models\ModelUploadFile;
16+
use Maatwebsite\Excel\Concerns\WithBatchInserts;
17+
use Maatwebsite\Excel\Concerns\WithChunkReading;
18+
use FromHome\ModelUpload\Models\ModelUploadRecord;
19+
use Maatwebsite\Excel\Concerns\SkipsUnknownSheets;
20+
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
21+
use FromHome\ModelUpload\Jobs\ProcessModelRecordJob;
22+
23+
final class ModelRecordImport implements ShouldQueue, SkipsUnknownSheets, ToModel, WithBatchInserts, WithChunkReading, WithEvents, WithHeadingRow, WithMultipleSheets
24+
{
25+
use Importable;
26+
27+
private ?ModelUploadFile $uploadFile = null;
28+
29+
private array $meta = [];
30+
31+
public static function new(): self
32+
{
33+
return new self();
34+
}
35+
36+
public function forFile(ModelUploadFile $uploadFile): self
37+
{
38+
$this->uploadFile = $uploadFile;
39+
40+
return $this;
41+
}
42+
43+
public function withMeta(array $meta): self
44+
{
45+
$this->meta = $meta;
46+
47+
return $this;
48+
}
49+
50+
public function process(): PendingDispatch
51+
{
52+
Assert::notNull($this->uploadFile);
53+
54+
return $this->queue(
55+
$this->uploadFile->getAttribute('file_path'),
56+
$this->uploadFile->getAttribute('storage_disk'),
57+
);
58+
}
59+
60+
public function model(array $row): ModelUploadRecord
61+
{
62+
Assert::notNull($this->uploadFile);
63+
64+
return new ModelUploadRecord([
65+
'model_upload_file_id' => $this->uploadFile->getKey(),
66+
'payload' => $row,
67+
'meta' => $this->meta,
68+
]);
69+
}
70+
71+
public function batchSize(): int
72+
{
73+
return 500;
74+
}
75+
76+
public function chunkSize(): int
77+
{
78+
return 500;
79+
}
80+
81+
public function registerEvents(): array
82+
{
83+
return [
84+
AfterImport::class => function (): void {
85+
Assert::notNull($this->uploadFile);
86+
87+
dispatch(new ProcessModelRecordJob($this->uploadFile));
88+
},
89+
];
90+
}
91+
92+
public function sheets(): array
93+
{
94+
return [
95+
'DATA' => $this,
96+
];
97+
}
98+
99+
public function onUnknownSheet($sheetName): void
100+
{
101+
}
102+
}

src/ModelUpload.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,32 @@
44

55
namespace FromHome\ModelUpload;
66

7+
use Illuminate\Http\Request;
8+
use Illuminate\Http\UploadedFile;
9+
use FromHome\ModelUpload\Models\ModelUploadFile;
10+
use FromHome\ModelUpload\Actions\StoreModelUploadFile;
11+
use FromHome\ModelUpload\Processor\RecordProcessorManager;
12+
713
final class ModelUpload
814
{
15+
public static function registerRecordProcessors(array $processors): void
16+
{
17+
/** @var RecordProcessorManager $manager */
18+
$manager = app(RecordProcessorManager::class);
19+
20+
$manager->registerRecordProcessors($processors);
21+
}
22+
23+
public static function storeModelUploadFile(Request $request, array $meta = []): ModelUploadFile
24+
{
25+
/** @var StoreModelUploadFile $action */
26+
$action = app(StoreModelUploadFile::class);
27+
28+
/** @var UploadedFile $file */
29+
$file = $request->file('file');
30+
31+
return $action->handle(
32+
$request->user(), $file, $request->input('model_type'), $meta
33+
);
34+
}
935
}

src/ModelUploadServiceProvider.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,20 @@
66

77
use Spatie\LaravelPackageTools\Package;
88
use Spatie\LaravelPackageTools\PackageServiceProvider;
9+
use FromHome\ModelUpload\Actions\StoreModelUploadFile;
10+
use FromHome\ModelUpload\Processor\RecordProcessorManager;
911

1012
final class ModelUploadServiceProvider extends PackageServiceProvider
1113
{
14+
public function registeringPackage(): void
15+
{
16+
$this->app->singleton(StoreModelUploadFile::class);
17+
$this->app->singleton(RecordProcessorManager::class);
18+
}
19+
1220
public function configurePackage(Package $package): void
1321
{
14-
$package
15-
->name('laravel-model-upload')
16-
->hasConfigFile()
22+
$package->name('laravel-model-upload')
1723
->hasMigration('create_model_upload_table');
1824
}
1925
}

0 commit comments

Comments
 (0)