1

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 each line of the array contains exactly the same type of data as provided in my form. Therefore it can use the same validation rules.

I am a little lost how to actually implement this; the data parsed from the CSV is in an array, and not the 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.

1
  • You should not edit your question to include an answer, instead you are encouraged to self-answer if you came up with a solution on your own. Commented Feb 19, 2020 at 0:16

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you. This is interesting but it feels quite complicated to just convert an array into a format that can go through the Laravels form request validation. For now, I have decided to use the $validator = Validator::make($row, $rules); function and run this on each row of the array, which is parsed from the CSV. The downside is that I now have a form request validation and an array validation which is duplicated code. I guess I could easily split out the rules array to use this in the two places, but its OK for now.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.