3

I'm a newbie to voyager, is there a way I can upload users via CSV in voyager. I imagine I would have to make a custom template for this, anyone that can point me on the right direction on this, I would highly appreciate it.

Jayjay
  • 112
  • 1
  • 1
  • 9

1 Answers1

1

I know this is an old question, but since this is the first thing I encounter when I research the same thing, I wanted to add an answer so that those who come here can benefit.

This solution is for Laravel 8.9.0 and Voyager 1.4

This has a selection before uploading CSV, it is not necessary but good to have, so you don't have to adjust CSV before uploading.

I did use another resource to do it, which was in Laravel 5.5 you can see the original codes from Laravel-5.5 CSV Import

I'll also add a fork which updated with Laravel 9.0 after I finished my own project.

Anyhow, I use Laravel-Excel 3.1.24 to read CSV. The resource was using an older version.

Let's Start the Coding

Before you connect it with Voyager, first you should know that you need to write your view and controller to do the job.

Also, we'll need our model. For your User Model, make sure that you added all the fields you need. Create migration with the same needs.

Like if your User Model has this;

protected $fillable = [
    'name',
    'email',
    'password',
];

Make sure your migration;

public function up()
{
    Schema::create('users', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->string('email')->unique();
        $table->timestamp('email_verified_at')->nullable();
        $table->string('password');
        $table->rememberToken();
        $table->timestamps();
    });
       }

After you set your model and migration, you'll need one other model to store your CSV. This will be helpful for you while doing the selection with CSV and database fields. It will have only three fields; 'csv_filename', 'csv_header', 'csv_data' .

After setting our models, we'll need to add config/app.php to our selections.

    'db_fields' => [
    'name',
    'email',
    'password',
]

That fields we will get from CSV, you can remove the password if you don't have password in your CSV file.

Before getting into our Controller, we should add Laravel-Excel. From the documentation, all you need to do is run this command on the terminal.

composer require "maatwebsite/excel:3.1.24"

This will add Laravel-Excel to our project. Our controller will need "import" which is a class to helps us import files, comes with Laravel-Excel. For this;

php artisan make:import CsvImport --model=User

You can use it for a quick start like Laravel-Excel documentation recommended. Now we can get into our Controller. We'll create controller vie Laravel commands. We're gonna need bunch of things;

 use App\Imports\CsvImport; //This is needed for Laravel-Excel.
 use Illuminate\Http\Request; 
 use App\Models\User;  //Our user model.
 use App\Models\CsvData; //Our model for csv.
 use App\Http\Requests\CsvImportRequest; //I'll share this in a sec.
 use Maatwebsite\Excel\Facades\Excel; //This is from Laravel-Excel
 use Illuminate\Support\Str; //This is for basic password creation

 class ImportController extends Controller
 {

The functions we need are;

    public function form()
{
    return view('import.form');
}

This function is pretty simple. It will return our view. The second one is when we parse our CSV and saving in it add CSV_Data. But first, we need a request as Laravel wanted.

 php artisan make:request CSVImportRequest

I'll share mine;

class CsvImportRequest extends FormRequest
{
     public function authorize()
{
    return true;
}

/**
 * Get the validation rules that apply to the request.
 *
 * @return array
 */
public function rules()
{
    return [
        'csv_file' => 'required|file'
    ];
}

Now we can go back to our Controller;

    public function parseImport(CsvImportRequest $request)
{
    //we getting with the request the file. So you need to create request with 
 //Laravel. And you should add this to your Controller as use App\Http\Requests\CsvImportRequest;

    $path = $request->file('csv_file')->getRealPath();
    if ($request->has('header')) {
        //this is coming from Laravel-Excel package. Make sure you added to your 
//controller; use Maatwebsite\Excel\Facades\Excel;
        $data = Excel::toArray(new CsvImport, request()->file('csv_file'))[0];
    } else {
        $data = array_map('str_getcsv', file($path));
    }

    if (count($data) > 0) {
       //checking if the header option selected
        if ($request->has('header')) {
            $csv_header_fields = [];
            foreach ($data[0] as $key => $value) {
                $csv_header_fields[] = $key;
            }
        }
        $csv_data = array_slice($data, 0, 2);
         //creating csvdata for our database
        $csv_data_file = CsvData::create([
            'csv_filename' => $request->file('csv_file')->getClientOriginalName(),
            'csv_header' => $request->has('header'),
            'csv_data' => json_encode($data)
        ]);
    } else {
        return redirect()->back();
    }
    //this is the view when we go after we submit our form.We're sending our data so we can select to match with db_fields.
    return view('import.fields', compact('csv_header_fields', 'csv_data', 'csv_data_file'));

}

And, now the import function.

    public function processImport(Request $request)
{//we are getting data from request to match the fields.
    $data = CsvData::find($request->csv_data_file_id);
    $csv_data = json_decode($data->csv_data, true);
    $request->fields = array_flip($request->fields);
    foreach ($csv_data as $row) {
        $contact = new User();
        foreach (config('app.db_fields') as $index => $field) {
        //using config app.db_fields while matching with request fields
            if ($data->csv_header) {
                if ($field == "null") {
                    continue;
                } else if ($field == "password") {
                      //this is checkin if password option is set. If not, it is creating a password. You can eliminate this according to your needs.
                    if (isset($request->fields['password'])) {
                        $pw = $row[$request->fields['password']];
                    } else {
                        $pw = Str::random(10);
                    }
                } else
                    $contact->$field = $row[$request->fields[$field]];
            } else 
//same with the if but without headers. You can create a function to avoid writing 
//codes twice.
{
                if ($field == "null") {
                    continue;
                } else if ($field == "password") {
                    if (isset($request->fields['password'])) {
                        $pw = $row[$request->fields['password']];
                    } else {
                        $pw = Str::random(10);
                    }
                } else
                    $contact->$field = $row[$request->fields[$index]];

            }
        }
        $user = User::where(['email' => $contact->email])->first();
 //checking for duplicate
        if (empty($user)) {
            $contact->password = bcrypt($pw);
            $contact->save();

        } else {
            $duplicated[] = $contact->email;
            //if you want you can keep the duplicated ones to check which ones are duplicated
        }

    }
 //you can redirect wherever you want. I didn't need an success view so I returned 
//voyagers original users view to see my data.
    return redirect(route('voyager.users.index'));
}

Now, we can create our routes. Since we're using Voyager do not forget to add voyager., so we can use it with the voyager admin panel.

 use App\Http\Controllers\ImportController;

 Route::group(['prefix' => 'admin','as' => 'voyager.', 'middleware' => 'admin.user'], 
function()
{
Route::get('import',[ImportController::class, 'form'])->name("import.form");
Route::post('import/parse', [ImportController::class, 'parseImport'])- 
>name("import.parse");
Route::post('import/process', [ImportController::class, 'processImport'])- 
>name("import.process");
});

Also, we'll need a view while importing and selecting. I created my views in the views/import.

For your form blade, you want to use it with voyager so you should

@extends('voyager::master')

use this to have the same theme. After that, you need to add your html with a section.

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">CSV Import</div>

                <div class="panel-body">
                    <form class="form-horizontal" method="POST" action="{{ route('voyager.import.parse') }}" enctype="multipart/form-data">
                        {{ csrf_field() }}

                        <div class="form-group{{ $errors->has('csv_file') ? ' has-error' : '' }}">
                            <label for="csv_file" class="col-md-4 control-label">CSV file to import</label>

                            <div class="col-md-6">
                                <input id="csv_file" type="file" class="form-control" name="csv_file" required>

                                @if ($errors->has('csv_file'))
                                    <span class="help-block">
                                    <strong>{{ $errors->first('csv_file') }}</strong>
                                </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group">
                            <div class="col-md-6 col-md-offset-4">
                                <div class="checkbox">
                                    <label>
                                        <input type="checkbox" name="header" checked> File contains header row?
                                    </label>
                                </div>
                            </div>
                        </div>

                        <div class="form-group">
                            <div class="col-md-8 col-md-offset-4">
                                <button type="submit" class="btn btn-primary">
                                    Parse CSV
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

As you can see, we use "voyager." to add our route. This will also help us to Voyager settings. Our second view is where we choose our db_fields.

@extends('voyager::master')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">CSV Import</div>
                <div class="panel-body">
                    <form class="form-horizontal" method="POST" action="{{ route('voyager.import.process') }}">
                        {{ csrf_field() }}
                        <input type="hidden" name="csv_data_file_id" value="{{ $csv_data_file->id }}" />

                        <table class="table">
                            @if (isset($csv_header_fields))
                                <tr>
                                    @foreach ($csv_header_fields as $csv_header_field)
                                        <th>{{ $csv_header_field }}</th>
                                    @endforeach
                                </tr>
                            @endif
                            @foreach ($csv_data as $row)
                                <tr>
                                    @foreach ($row as $key => $value)
                                        <td>{{ $value }}</td>
                                    @endforeach
                                </tr>
                            @endforeach
                            <tr>
                                @foreach ($csv_data[0] as $key => $value)
                                    <td>
                                        <select name="fields[{{ $key }}]">
                                            <option value="null">Do Not Save</option>
                                        @foreach (config('app.db_fields') as $db_field)
                                                <option value="{{ (\Request::has('header')) ? $db_field : $loop->index }}"
                                                        @if ($key === $db_field) selected @endif>{{ $db_field }}</option>
                                            @endforeach
                                        </select>
                                    </td>
                                @endforeach
                            </tr>
                        </table>

                        <button type="submit" class="btn btn-primary">
                            Import Data
                        </button>
                    </form>
                </div>
            </div>

        </div>
    </div>
</div>
@endsection

This is it! Now we have our import set. We just need to configure with Voyager.

VOYAGER SETTINGS

First, we need a menu-item to reach our import view. After you get into Voyager panel, you need to go to the menu builder to create a menu item. All you have to do is make the url of the item as /admin/import with that when you clicked that item, you'll go to our import form view. For the other options, you can change them however you want.

You may also need to change from your user BREAD Model as App\Models\User, so it can receive all the fields we created.