Skip to content

Commit e4097ee

Browse files
committed
Add enhanced import functionality with validation and error handling
- Introduced EnhancedDefaultImport and EnhancedDefaultRelationshipImport classes for improved import capabilities. - Added methods to stop imports with user-friendly messages for errors, warnings, information, and success. - Implemented header and custom validation checks in import processes. - Updated language files for new import status messages. - Created example solutions demonstrating usage of enhanced imports with validation. - Added tests to ensure proper functionality of stopping imports and validation checks.
1 parent 67e4606 commit e4097ee

File tree

9 files changed

+963
-13
lines changed

9 files changed

+963
-13
lines changed

README.md

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -334,10 +334,134 @@ class PostsRelationManager extends RelationManager
334334
}
335335
```
336336

337-
Everything behaves and can be modified similar to the `ExcelImportAction` class, except the `DefaultRelationshipImport` class is used instead of the `DefaultImport` class. So if you are implementing a custom import class, you will need to extend the `DefaultRelationshipImport` class instead of the `DefaultImport` class.
337+
### Stopping Imports and Returning Messages
338338

339+
**New in v3.1.5+**: You can now stop the import process from within your custom import class and return messages to the frontend. This is useful for:
340+
341+
- Header validation to ensure the file format is correct
342+
- Business logic validation that might require stopping the entire import
343+
- Custom validation that depends on form data or external conditions
344+
345+
#### Using Enhanced Import Classes
346+
347+
The package now provides `EnhancedDefaultImport` and `EnhancedDefaultRelationshipImport` classes that include methods to stop imports:
348+
349+
```php
350+
use EightyNine\ExcelImport\EnhancedDefaultImport;
351+
use Illuminate\Support\Collection;
352+
353+
class CustomUserImport extends EnhancedDefaultImport
354+
{
355+
protected function beforeCollection(Collection $collection): void
356+
{
357+
// Validate required headers
358+
$requiredHeaders = ['name', 'email', 'phone'];
359+
$this->validateHeaders($requiredHeaders, $collection);
360+
361+
// Custom business logic validation
362+
if ($collection->count() > 1000) {
363+
$this->stopImportWithError('Too many rows. Maximum 1000 allowed.');
364+
}
365+
366+
// Access custom data from the form
367+
$formData = $this->customImportData;
368+
if (isset($formData['department_id'])) {
369+
$departmentExists = Department::where('id', $formData['department_id'])->exists();
370+
$this->validateCustomCondition(
371+
$departmentExists,
372+
'Selected department does not exist.'
373+
);
374+
}
375+
}
376+
377+
protected function beforeCreateRecord(array $data, $row): void
378+
{
379+
// Row-level validation
380+
if (User::where('email', $data['email'])->exists()) {
381+
$this->stopImportWithWarning(
382+
"User with email {$data['email']} already exists."
383+
);
384+
}
385+
}
386+
387+
protected function afterCollection(Collection $collection): void
388+
{
389+
// Show success message with statistics
390+
$count = $collection->count();
391+
$this->stopImportWithSuccess("Successfully imported {$count} users!");
392+
}
393+
}
394+
```
395+
396+
#### Available Methods
397+
398+
**Stop Import Methods:**
399+
- `stopImportWithError(string $message)` - Shows red error notification
400+
- `stopImportWithWarning(string $message)` - Shows orange warning notification
401+
- `stopImportWithInfo(string $message)` - Shows blue info notification
402+
- `stopImportWithSuccess(string $message)` - Shows green success notification
403+
404+
**Validation Helpers:**
405+
- `validateHeaders(array $expectedHeaders, Collection $collection)` - Validates required headers
406+
- `validateCustomCondition(bool $condition, string $errorMessage)` - Custom validation
407+
408+
**Hook Methods (override in your class):**
409+
- `beforeCollection(Collection $collection)` - Called before processing starts
410+
- `beforeCreateRecord(array $data, $row)` - Called before each record creation
411+
- `afterCreateRecord(array $data, $row, $record)` - Called after each record creation
412+
- `afterCollection(Collection $collection)` - Called after processing completes
413+
414+
#### Using with Form Data
415+
416+
You can access custom form data in your import class for validation:
417+
418+
```php
419+
protected function getHeaderActions(): array
420+
{
421+
return [
422+
\EightyNine\ExcelImport\ExcelImportAction::make()
423+
->use(CustomUserImport::class)
424+
->beforeUploadField([
425+
Select::make('department_id')
426+
->label('Department')
427+
->options(Department::pluck('name', 'id'))
428+
->required(),
429+
])
430+
->beforeImport(function (array $data, $livewire, $excelImportAction) {
431+
$excelImportAction->customImportData([
432+
'department_id' => $data['department_id'],
433+
'imported_by' => auth()->id(),
434+
]);
435+
}),
436+
];
437+
}
438+
```
439+
440+
#### Using with Existing Import Classes
441+
442+
If you have existing import classes, you can add stopping capabilities by throwing the `ImportStoppedException`:
443+
444+
```php
445+
use EightyNine\ExcelImport\Exceptions\ImportStoppedException;
446+
447+
class MyExistingImport implements ToCollection, WithHeadingRow
448+
{
449+
public function collection(Collection $collection)
450+
{
451+
// Your existing validation logic
452+
if ($someCondition) {
453+
throw new ImportStoppedException('Custom error message', 'error');
454+
}
455+
456+
// Continue with normal processing...
457+
}
458+
}
459+
```
460+
461+
The exception constructor accepts:
462+
- `$message` (string) - The message to show to the user
463+
- `$type` (string) - The notification type: 'error', 'warning', 'info', or 'success' (default: 'error')
339464

340-
## Testing
341465

342466
```bash
343467
composer test

example-solution.php

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
<?php
2+
3+
// Example 1: Custom Import with Header Validation
4+
// ================================================
5+
6+
use EightyNine\ExcelImport\EnhancedDefaultImport;
7+
use Illuminate\Support\Collection;
8+
9+
class CustomUserImport extends EnhancedDefaultImport
10+
{
11+
protected function beforeCollection(Collection $collection): void
12+
{
13+
// Example 1: Validate required headers
14+
$requiredHeaders = ['name', 'email', 'phone'];
15+
$this->validateHeaders($requiredHeaders, $collection);
16+
17+
// Example 2: Custom validation using form data
18+
$formData = $this->customImportData; // Access custom data from the form
19+
20+
// Example: Check if a specific condition from the form is met
21+
if (isset($formData['department_id'])) {
22+
$departmentExists = \App\Models\Department::where('id', $formData['department_id'])->exists();
23+
$this->validateCustomCondition(
24+
$departmentExists,
25+
'The selected department does not exist. Please select a valid department.'
26+
);
27+
}
28+
29+
// Example 3: File-level validation (e.g., maximum number of rows)
30+
if ($collection->count() > 1000) {
31+
$this->stopImportWithError('Import file contains too many rows. Maximum allowed is 1000 rows.');
32+
}
33+
34+
// Example 4: Validate file content based on business logic
35+
$hasValidData = $collection->every(function ($row) {
36+
return !empty($row['name']) && !empty($row['email']);
37+
});
38+
39+
if (!$hasValidData) {
40+
$this->stopImportWithError('All rows must have both name and email filled.');
41+
}
42+
}
43+
44+
protected function beforeCreateRecord(array $data, $row): void
45+
{
46+
// Example: Row-level validation with custom logic
47+
if (isset($data['email'])) {
48+
$existingUser = \App\Models\User::where('email', $data['email'])->first();
49+
if ($existingUser) {
50+
$this->stopImportWithWarning(
51+
"User with email {$data['email']} already exists. Import stopped to prevent duplicates."
52+
);
53+
}
54+
}
55+
56+
// Example: Validate against custom business rules
57+
if (isset($data['age']) && $data['age'] < 18) {
58+
$this->stopImportWithError(
59+
'All users must be 18 years or older. Found user with age: ' . $data['age']
60+
);
61+
}
62+
}
63+
64+
protected function afterCollection(Collection $collection): void
65+
{
66+
// Example: Show success message with statistics
67+
$count = $collection->count();
68+
$this->stopImportWithSuccess("Successfully imported {$count} users!");
69+
}
70+
}
71+
72+
// Example 2: Using the Custom Import in a Filament Resource
73+
// =========================================================
74+
75+
use App\Filament\Resources\UserResource;
76+
use Filament\Actions;
77+
use Filament\Forms\Components\Select;
78+
use Filament\Forms\Components\TextInput;
79+
use Filament\Resources\Pages\ListRecords;
80+
81+
class ListUsers extends ListRecords
82+
{
83+
protected static string $resource = UserResource::class;
84+
85+
protected function getHeaderActions(): array
86+
{
87+
return [
88+
\EightyNine\ExcelImport\ExcelImportAction::make()
89+
->color("primary")
90+
->use(CustomUserImport::class)
91+
92+
// Add custom form fields for additional validation
93+
->beforeUploadField([
94+
Select::make('department_id')
95+
->label('Department')
96+
->options(\App\Models\Department::pluck('name', 'id'))
97+
->required()
98+
->helperText('Users will be assigned to this department'),
99+
100+
TextInput::make('batch_name')
101+
->label('Batch Name')
102+
->required()
103+
->helperText('Name for this import batch for tracking'),
104+
])
105+
106+
// Pass the form data to the custom import
107+
->beforeImport(function (array $data, $livewire, $excelImportAction) {
108+
// Pass custom data from the form to the import class
109+
$excelImportAction->customImportData([
110+
'department_id' => $data['department_id'],
111+
'batch_name' => $data['batch_name'],
112+
'imported_by' => auth()->id(),
113+
'import_timestamp' => now(),
114+
]);
115+
116+
// Add additional data that will be merged with each row
117+
$excelImportAction->additionalData([
118+
'department_id' => $data['department_id'],
119+
'status' => 'active',
120+
'imported_by' => auth()->id(),
121+
]);
122+
}),
123+
124+
Actions\CreateAction::make(),
125+
];
126+
}
127+
}
128+
129+
// Example 3: Custom Relationship Import with Validation
130+
// =====================================================
131+
132+
use EightyNine\ExcelImport\EnhancedDefaultRelationshipImport;
133+
134+
class CustomPostImport extends EnhancedDefaultRelationshipImport
135+
{
136+
protected function beforeCollection(Collection $collection): void
137+
{
138+
// Validate headers for posts
139+
$requiredHeaders = ['title', 'content', 'category'];
140+
$this->validateHeaders($requiredHeaders, $collection);
141+
142+
// Check if the user has permission to create posts in this category
143+
$categories = $collection->pluck('category')->unique();
144+
foreach ($categories as $category) {
145+
$categoryExists = \App\Models\Category::where('name', $category)->exists();
146+
$this->validateCustomCondition(
147+
$categoryExists,
148+
"Category '{$category}' does not exist. Please ensure all categories exist before importing."
149+
);
150+
}
151+
}
152+
153+
protected function beforeCreateRecord(array $data, $row): void
154+
{
155+
// Validate that the post title is unique for this user
156+
$existingPost = $this->ownerRecord->posts()
157+
->where('title', $data['title'])
158+
->exists();
159+
160+
if ($existingPost) {
161+
$this->stopImportWithError(
162+
"Post with title '{$data['title']}' already exists for this user."
163+
);
164+
}
165+
}
166+
}
167+
168+
// Example 4: Advanced Validation with Multiple Conditions
169+
// =======================================================
170+
171+
class AdvancedProductImport extends EnhancedDefaultImport
172+
{
173+
protected function beforeCollection(Collection $collection): void
174+
{
175+
// Multiple validation checks
176+
$this->validateHeaders(['sku', 'name', 'price', 'category'], $collection);
177+
178+
// Check for duplicate SKUs in the file itself
179+
$skus = $collection->pluck('sku');
180+
$duplicateSkus = $skus->duplicates();
181+
182+
if ($duplicateSkus->isNotEmpty()) {
183+
$this->stopImportWithError(
184+
'Duplicate SKUs found in the file: ' . $duplicateSkus->implode(', ')
185+
);
186+
}
187+
188+
// Validate all prices are numeric and positive
189+
$invalidPrices = $collection->filter(function ($row) {
190+
return !is_numeric($row['price']) || $row['price'] <= 0;
191+
});
192+
193+
if ($invalidPrices->isNotEmpty()) {
194+
$this->stopImportWithError(
195+
'Invalid prices found. All prices must be positive numbers.'
196+
);
197+
}
198+
199+
// Business logic validation using custom import data
200+
if (isset($this->customImportData['supplier_id'])) {
201+
$supplier = \App\Models\Supplier::find($this->customImportData['supplier_id']);
202+
if (!$supplier || !$supplier->is_active) {
203+
$this->stopImportWithError(
204+
'The selected supplier is not active. Cannot import products.'
205+
);
206+
}
207+
}
208+
}
209+
210+
protected function beforeCreateRecord(array $data, $row): void
211+
{
212+
// Check if SKU already exists in database
213+
if (\App\Models\Product::where('sku', $data['sku'])->exists()) {
214+
$this->stopImportWithWarning(
215+
"Product with SKU '{$data['sku']}' already exists. Skipping import to prevent conflicts."
216+
);
217+
}
218+
}
219+
}
220+
221+
// Example 5: Using in Filament with Enhanced Error Handling
222+
// =========================================================
223+
224+
protected function getHeaderActions(): array
225+
{
226+
return [
227+
\EightyNine\ExcelImport\ExcelImportAction::make()
228+
->color("primary")
229+
->use(AdvancedProductImport::class)
230+
->beforeUploadField([
231+
Select::make('supplier_id')
232+
->label('Supplier')
233+
->options(\App\Models\Supplier::where('is_active', true)->pluck('name', 'id'))
234+
->required(),
235+
236+
TextInput::make('import_notes')
237+
->label('Import Notes')
238+
->maxLength(500),
239+
])
240+
->beforeImport(function (array $data, $livewire, $excelImportAction) {
241+
$excelImportAction->customImportData([
242+
'supplier_id' => $data['supplier_id'],
243+
'import_notes' => $data['import_notes'],
244+
'imported_by' => auth()->user()->name,
245+
]);
246+
247+
$excelImportAction->additionalData([
248+
'supplier_id' => $data['supplier_id'],
249+
'created_by' => auth()->id(),
250+
]);
251+
})
252+
->sampleExcel([
253+
['sku' => 'PROD001', 'name' => 'Sample Product', 'price' => '29.99', 'category' => 'Electronics'],
254+
['sku' => 'PROD002', 'name' => 'Another Product', 'price' => '49.99', 'category' => 'Books'],
255+
]),
256+
];
257+
}

0 commit comments

Comments
 (0)