Laravel CSV导入 - 我可以使用请求验证吗?

I am using Laravel 5.6 and have setup a form request validation for my form which submits a single row, validates it and then adds to the database. This all works fine.

For batch import of multiple rows I have a CSV import. The CSV is parsed into an array and then each line of the array contains exactly the same type of data as provided in my form and therefore can use the same validation rules.

I am a little lost how to actually implement this as the data, once parsed from the CSV is in an array and not a request object that the form validation request is looking for.

Does anyone have any tips on the best way to be able to use my form validation for both the form and CSV without duplicating code?

EDIT

If anyone is interested, my final solution was to not use the form request validation. In my case, it was easier to add the validation rules and messages to some protected functions inside the controller. This means that they can be re-used across each of the controller functions that need it (store, csvStore etc.) without duplicate code. In this case, I am not sure what advantages the form request validation feature gives.

//reformat CSV into array
$master = [];
$line_id = 1;
foreach ($data as $row) {
    //skip blank rows
    if (empty($row['sku'])) continue;
    //build master
    foreach($row as $key => $value){
        if(!empty($value)) $master[$row['sku']][$key] = $row[$key];
    }
    //add line number for debugging
    $master[$row['sku']]['line_number'] = $line_id;
    $line_id++;
}

//Validate each row of CSV individually
$error_messages = new MessageBag();
$error_count = 0;
$duplicate_count = 0;
if(empty($master)){
    //empty $master
    $error_messages->add('', 'CSV file does not contain valid data or is empty');
    flash()->message('Nothing was imported');
    return redirect()->back()->withErrors($error_messages);
} else {
    foreach($master as $row){
        $validator = Validator::make($row,$this->createValidationRules(), $this->createValidationMessages());

        //Check validation
        if ($validator->fails()){
            $master[$row['sku']]['valid'] = false;
            if(isset($validator->failed()['sku']['Unique'])){
                $duplicate_count ++;
                if(!request('ignore-duplicates') && !request('ignore-errors')) $error_messages->merge($validator->errors()->messages()); //save error messages
            } else {
                $error_count ++;
                if(!request('ignore-errors')) $error_messages->merge($validator->errors()->messages()); //save error messages
            }
        } else {
            $master[$row['sku']]['valid'] = true;
        }
    }
}

//add successful rows to DB
$success_count = 0;
foreach($master as $row){
    if($row['valid'] == true){
        $productCreate = new ProductCreate();
        $productCreate->create($row);
        $success_count++;
    }
}

I then used the success/error/duplicate counts to send back a suitable error message bag and/or flash messages.

You could approach it by creating a Request object macro to turn the CSV into an array, then use middleware to parse an incoming request if it's a csv file and merge it into the incoming request. Then your application's validation can validate it using array validation.

Start by making the service provider to house your request macro:

php artisan make:provider RequestMacroParseCsvProvider

Then in the service provider:

Add this at the top to pull in the Request class:

use Illuminate\Http\Request;

Inside the register method of the provider:

Request::macro('parseCsv', function ($fileNameKey) {
    // Note: while working inside of the request macro closure, you can access the request object by referencing $this->{key_of_request_item}

    // You will be running your parser against $fileNameKey which will be the key of the request file coming in. So you'd access it like:
    if ($this->hasFile($fileNameKey)) {
        // Your code to parse the csv would go here. Instantiate your csv parsing class or whatever...
        // $file = $this->file($fileNameKey);
        // Store the parsed csv in an array, maybe named $parsedCsv?
    }

    return empty($parsedCsv) ? [] : $parsedCsv;
});

Register the service provider in your config/app.php

App\Providers\RequestMacroParseCsvProvider::class,

Create a middleware to check if the incoming request contains a csv

php artisan make:middleware MergeCsvArrayIntoRequest

In the handle method:

if ($request->has('your_csv_request_key)) {
    $parsedCsv = $request->parseCsv('your_csv_request_key');

    // Then add it into the request with a key of 'parsedCsv' or whatever you want to call it
    $request->merge(['parsedCsv' => $parsedCsv]);
}

return $next($request);

Register your middleware in your app/Http/Kernel.php:

protected $middleware = [
    ...
    \App\Http\Middleware\MergeCsvArrayIntoRequest::class,
    ...
];

Or put it into $routeMiddleware if you don't want it to be global.

'parse.csv' => \App\Http\Middleware\MergeCsvArrayIntoRequest::class,

Now, your middleware is intercepting and converting any CSV files you upload and you can validate parsedCsv request key using Laravel's array validation.

You can definitely make some improvements to make it more flexible, if you want. I've done something similar, but not quite file related, in another project where I needed to modify the request before it got to my controller's validation and it worked.

Hope this helps.